diff --git a/.github/ci-prerequisites.sh b/.github/ci-prerequisites.sh
index 94a0a47e575523..5c8773b20939d1 100755
--- a/.github/ci-prerequisites.sh
+++ b/.github/ci-prerequisites.sh
@@ -41,8 +41,19 @@ time sudo rm -rf /usr/share/swift || true
time sudo rm -rf /usr/local/lib/android || true
# Remove Haskell
time sudo rm -rf /opt/ghc || true
+time sudo rm -rf /usr/local/.ghcup || true
# Remove pipx
time sudo rm -rf /opt/pipx || true
+# Remove Rust
+time sudo rm -rf /usr/share/rust || true
+# Remove Go
+time sudo rm -rf /usr/local/go || true
+# Remove miniconda
+time sudo rm -rf /usr/share/miniconda || true
+# Remove powershell
+time sudo rm -rf /usr/local/share/powershell || true
+# Remove Google Cloud SDK
+time sudo rm -rf /usr/lib/google-cloud-sdk || true
# Remove infrastructure things that are unused and take a lot of space
time sudo rm -rf /opt/hostedtoolcache/CodeQL || true
diff --git a/.github/native-tests.json b/.github/native-tests.json
index 11d2eca78c18ac..828044748e0c24 100644
--- a/.github/native-tests.json
+++ b/.github/native-tests.json
@@ -128,8 +128,8 @@
},
{
"category": "gRPC",
- "timeout": 70,
- "test-modules": "grpc-health, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-tls",
+ "timeout": 75,
+ "test-modules": "grpc-health, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-tls, grpc-tls-p12",
"os-name": "ubuntu-latest"
},
{
diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml
index ae7233040250eb..f993137b1ecb46 100644
--- a/.github/quarkus-github-bot.yml
+++ b/.github/quarkus-github-bot.yml
@@ -383,13 +383,13 @@ triage:
- id: scheduler
labels: [area/scheduler]
title: "schedule(r)?"
- notify: [mkouba]
+ notify: [mkouba, manovotn]
directories:
- extensions/scheduler/
- id: quartz
labels: [area/scheduler]
title: "quartz"
- notify: [mkouba, machi1990]
+ notify: [mkouba, machi1990, manovotn]
directories:
- extensions/quartz/
- integration-tests/quartz/
diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml
index 11c9a43357c298..a34518ee35d893 100644
--- a/.github/workflows/ci-actions-incremental.yml
+++ b/.github/workflows/ci-actions-incremental.yml
@@ -376,6 +376,9 @@ jobs:
- name: Clean Gradle temp directory
if: always()
run: devtools/gradle/gradlew --stop && rm -rf devtools/gradle/gradle-extension-plugin/build/tmp
+ - name: Analyze disk space
+ if: always() && !startsWith(matrix.java.os-name, 'windows') && !startsWith(matrix.java.os-name, 'macos')
+ run: .github/ci-disk-usage.sh
- name: Prepare failure archive (if maven failed)
if: failure()
run: find . -name '*-reports' -type d -o -name '*.log' | tar -czf test-reports.tgz -T -
@@ -1072,6 +1075,11 @@ jobs:
build-scan-capture-strategy: ON_DEMAND
job-name: "Native Tests - ${{matrix.category}}"
wrapper-init: true
+ - name: Cache Quarkus metadata
+ uses: actions/cache@v4
+ with:
+ path: '**/.quarkus/quarkus-prod-config-dump'
+ key: ${{ runner.os }}-quarkus-metadata
- name: Build
env:
TEST_MODULES: ${{matrix.test-modules}}
diff --git a/.github/workflows/podman-build.yml b/.github/workflows/podman-build.yml
index b62a09c77344b3..2f4811a99b2ae3 100644
--- a/.github/workflows/podman-build.yml
+++ b/.github/workflows/podman-build.yml
@@ -74,6 +74,7 @@ jobs:
| sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list > /dev/null
sudo apt-get update -qq
sudo apt-get -qq -y install podman
+ sudo bash -c "echo -e '[engine]\nservice_timeout=0' >> /etc/containers/containers.conf"
# Runs a single command using the runners shell
- name: Check podman
run: docker version
@@ -89,7 +90,7 @@ jobs:
key: q2maven-${{ steps.get-date.outputs.date }}
- name: Initial build
run: |
- ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -Dinvoker.skip -Dno-format -Dtcks -Prelocations clean install
+ ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -DskipDocs -Dinvoker.skip -Dskip.gradle.tests -Djbang.skip -Dtruststore.skip -Dno-format -Dtcks -Prelocations clean install
- name: Verify extension dependencies
shell: bash
run: ./update-extension-dependencies.sh $COMMON_MAVEN_ARGS
@@ -149,7 +150,7 @@ jobs:
- name: Build
shell: bash
# Despite the pre-calculated run_jvm flag, GIB has to be re-run here to figure out the exact submodules to build.
- run: ./mvnw $COMMON_MAVEN_ARGS install -Dsurefire.timeout=1200 -pl !integration-tests/gradle -pl !integration-tests/maven -pl !integration-tests/devtools -pl !docs $JVM_TEST_MAVEN_ARGS ${{ needs.build-jdk11.outputs.gib_args }}
+ run: ./mvnw $COMMON_MAVEN_ARGS install -Dsurefire.timeout=1200 -pl !integration-tests/gradle -pl !integration-tests/maven -pl !integration-tests/devmode -pl !integration-tests/devtools -Dno-test-kubernetes -pl !docs $JVM_TEST_MAVEN_ARGS ${{ steps.get-gib-args.outputs.gib_args }}
- name: Delete Local Artifacts From Cache
shell: bash
run: rm -r ~/.m2/repository/io/quarkus
diff --git a/.gitpod/Dockerfile b/.gitpod/Dockerfile
index ed3ace5b124b45..47b829d212849b 100644
--- a/.gitpod/Dockerfile
+++ b/.gitpod/Dockerfile
@@ -1,8 +1,8 @@
FROM gitpod/workspace-java-17
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \
- sdk install java 17.0.9-tem && \
- sdk use java 17.0.9-tem && \
+ sdk install java 17.0.10-tem && \
+ sdk use java 17.0.10-tem && \
yes | sdk install quarkus && \
rm -rf $HOME/.sdkman/archives/* && \
rm -rf $HOME/.sdkman/tmp/* "
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index 1cb948d1490672..08c504fc6bc7f1 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -2,11 +2,21 @@
com.gradlegradle-enterprise-maven-extension
- 1.20
+ 1.20.1com.gradlecommon-custom-user-data-maven-extension1.12.5
+
+ com.gradle
+ quarkus-build-caching-extension
+ 0.10
+
+
+ io.quarkus.develocity
+ quarkus-project-develocity-extension
+ 1.0.6
+
diff --git a/.mvn/gradle-enterprise-custom-user-data.groovy b/.mvn/gradle-enterprise-custom-user-data.groovy
deleted file mode 100644
index 0f9b416591ea7f..00000000000000
--- a/.mvn/gradle-enterprise-custom-user-data.groovy
+++ /dev/null
@@ -1,129 +0,0 @@
-
-// Configure build scan publication
-boolean publish = true
-if(session?.getRequest()?.getBaseDirectory() != null) {
- def testBuildPaths = [
- File.separator + 'target' + File.separator + 'codestart-test' + File.separator,
- File.separator + 'target' + File.separator + 'it' + File.separator,
- File.separator + 'target' + File.separator + 'test-classes' + File.separator,
- File.separator + 'target' + File.separator + 'test-project' + File.separator
- ]
- publish = testBuildPaths.every {testBuildPath -> !session.getRequest().getBaseDirectory().contains(testBuildPath) }
- if(!publish) {
- // do not publish a build scan for test builds
- log.debug("Disabling build scan publication for " + session.getRequest().getBaseDirectory())
-
- // change storage location on CI to avoid Develocity scan dumps with disabled publication to be captured for republication
- if (System.env.GITHUB_ACTIONS) {
- try {
- def storageLocationTmpDir = java.nio.file.Files.createTempDirectory(java.nio.file.Paths.get(System.env.RUNNER_TEMP), "buildScanTmp").toAbsolutePath()
- log.debug('Update storage location to ' + storageLocationTmpDir)
- gradleEnterprise.setStorageDirectory(storageLocationTmpDir)
- } catch (IOException e) {
- log.error('Temporary storage location directory cannot be created, the Build Scan will be published', e)
- }
- }
- }
-}
-buildScan.publishAlwaysIf(publish)
-buildScan.publishIfAuthenticated()
-
-// Add mvn command line
-def mvnCommand = ''
-if (System.env.MAVEN_CMD_LINE_ARGS) {
- mvnCommand = "mvn ${System.env.MAVEN_CMD_LINE_ARGS}".toString()
- buildScan.value('mvn command line', mvnCommand)
-}
-
-//Add github action information
-if (System.env.GITHUB_ACTIONS) {
- def jobId = System.env.GITHUB_JOB
-
- buildScan.value('gh-job-id', jobId)
- buildScan.value('gh-event-name', System.env.GITHUB_EVENT_NAME)
- buildScan.value('gh-ref-name', System.env.GITHUB_REF_NAME)
- buildScan.value('gh-actor', System.env.GITHUB_ACTOR)
- buildScan.value('gh-workflow', System.env.GITHUB_WORKFLOW)
- String jobCustomValues = System.env.GE_CUSTOM_VALUES
- if (jobCustomValues != null && !jobCustomValues.isBlank()) {
- for (String jobCustomValue : jobCustomValues.split(",")) {
- int index = jobCustomValue.indexOf('=')
- if (index <= 0) {
- continue
- }
- buildScan.value(jobCustomValue.substring(0, index).trim(), jobCustomValue.substring(index + 1).trim())
- }
- }
-
- List similarBuildsTags = new ArrayList<>()
-
- buildScan.tag(jobId)
- similarBuildsTags.add(jobId)
-
- buildScan.tag(System.env.GITHUB_EVENT_NAME)
- similarBuildsTags.add(System.env.GITHUB_EVENT_NAME)
-
- buildScan.tag(System.env.GITHUB_WORKFLOW)
- similarBuildsTags.add(System.env.GITHUB_WORKFLOW)
-
- String jobTags = System.env.GE_TAGS
- if (jobTags != null && !jobTags.isBlank()) {
- for (String tag : jobTags.split(",")) {
- buildScan.tag(tag.trim())
- similarBuildsTags.add(tag.trim())
- }
- }
-
- buildScan.link('Workflow run', System.env.GITHUB_SERVER_URL + '/' + System.env.GITHUB_REPOSITORY + '/actions/runs/' + System.env.GITHUB_RUN_ID)
-
- def prNumber = System.env.PULL_REQUEST_NUMBER
- if (prNumber != null && !prNumber.isBlank()) {
- buildScan.value('gh-pr', prNumber)
- buildScan.tag('pr-' + prNumber)
- similarBuildsTags.add('pr-' + prNumber)
-
- buildScan.link('Pull request', System.env.GITHUB_SERVER_URL + '/' + System.env.GITHUB_REPOSITORY + '/pull/' + prNumber )
- }
-
- similarBuildsTags.add(System.env.RUNNER_OS)
-
- buildScan.link('Similar builds', 'https://ge.quarkus.io/scans?search.tags=' + java.net.URLEncoder.encode(String.join(",", similarBuildsTags), "UTF-8").replace("+", "%20"))
-
- buildScan.buildScanPublished { publishedBuildScan -> {
- File target = new File("target")
- if (!target.exists()) {
- target.mkdir()
- }
- File gradleBuildScanUrlFile = new File("target/gradle-build-scan-url.txt")
- if (!gradleBuildScanUrlFile.exists()) {
- gradleBuildScanUrlFile.withWriter { out ->
- out.print(publishedBuildScan.buildScanUri)
- }
- }
- new File(System.env.GITHUB_STEP_SUMMARY).withWriterAppend { out ->
- out.println("\n[Build scan](${publishedBuildScan.buildScanUri})\n`${mvnCommand}`\n\n")
- }
- }
- }
-}
-
-// Check runtime Maven version and Maven Wrapper version are aligned
-def runtimeInfo = (org.apache.maven.rtinfo.RuntimeInformation) session.lookup("org.apache.maven.rtinfo.RuntimeInformation")
-def runtimeMavenVersion = runtimeInfo?.getMavenVersion()
-Properties mavenWrapperProperties = new Properties()
-File mavenWrapperPropertiesFile = new File(".mvn/wrapper/maven-wrapper.properties")
-if(mavenWrapperPropertiesFile.exists()) {
- mavenWrapperPropertiesFile.withInputStream {
- mavenWrapperProperties.load(it)
- }
- // assuming the wrapper properties contains:
- // distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/VERSION/apache-maven-VERSION-bin.zip
- if(regexp = mavenWrapperProperties."distributionUrl" =~ /.*\/apache-maven-(.*)-bin\.zip/) {
- def wrapperMavenVersion = regexp.group(1)
- if (runtimeMavenVersion && wrapperMavenVersion && wrapperMavenVersion != runtimeMavenVersion) {
- log.warn("Maven Wrapper is configured with a different version (" + wrapperMavenVersion + ") than the runtime version (" + runtimeMavenVersion + "). This will negatively impact build consistency and build caching.")
- buildScan.tag("misaligned-maven-version")
- buildScan.value("wrapper-maven-version", wrapperMavenVersion)
- }
- }
-}
diff --git a/.mvn/gradle-enterprise.xml b/.mvn/gradle-enterprise.xml
index a9479d5aaebca6..3d9c369b492e2e 100644
--- a/.mvn/gradle-enterprise.xml
+++ b/.mvn/gradle-enterprise.xml
@@ -26,7 +26,7 @@
- #{env['GRADLE_LOCAL_BUILD_CACHE'] != null and env['RELEASE_GITHUB_TOKEN'] == null and properties['no-build-cache'] == null}
+ #{env['RELEASE_GITHUB_TOKEN'] == null and properties['no-build-cache'] == null}#{env['RELEASE_GITHUB_TOKEN'] == null and properties['no-build-cache'] == null}
diff --git a/.sdkmanrc b/.sdkmanrc
index 12624dacd0b266..d5b65f8a6ca4bd 100644
--- a/.sdkmanrc
+++ b/.sdkmanrc
@@ -1,4 +1,4 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
-java=17.0.9-tem
+java=17.0.10-tem
mvnd=1.0-m7-m39
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e3933a5d32d156..527731b60c7be4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,57 +4,56 @@
We try to make it easy, and all contributions, even the smaller ones, are more than welcome. This includes bug reports,
fixes, documentation, examples... But first, read this page (including the small print at the end).
-* [Legal](#legal)
-* [Reporting an issue](#reporting-an-issue)
-* [Checking an issue is fixed in main](#checking-an-issue-is-fixed-in-main)
- + [Using snapshots](#using-snapshots)
- + [Building main](#building-main)
- + [Updating the version](#updating-the-version)
-* [Before you contribute](#before-you-contribute)
- + [Code reviews](#code-reviews)
- + [Coding Guidelines](#coding-guidelines)
- + [Continuous Integration](#continuous-integration)
- + [Tests and documentation are not optional](#tests-and-documentation-are-not-optional)
-* [Setup](#setup)
- + [IDE Config and Code Style](#ide-config-and-code-style)
- - [Eclipse Setup](#eclipse-setup)
- - [IDEA Setup](#idea-setup)
- * [How to work](#how-to-work)
- * [`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)
- - [Building a single module of an extension](#building-a-single-module-of-an-extension)
- - [Building with relocations](#building-with-relocations)
- - [Running a single test](#running-a-single-test)
- * [Maven Invoker tests](#maven-invoker-tests)
- + [Build with multiple threads](#build-with-multiple-threads)
- + [Don't build any test modules](#don-t-build-any-test-modules)
- - [Automatic incremental build](#automatic-incremental-build)
- * [Special case `bom-descriptor-json`](#special-case--bom-descriptor-json-)
- * [Usage by CI](#usage-by-ci)
-* [Release your own version](#release)
-* [Documentation](#documentation)
- + [Building the documentation](#building-the-documentation)
- + [Referencing a new guide in the index](#referencing-a-new-guide-in-the-index)
-* [Usage](#usage)
- - [With Maven](#with-maven)
- - [With Gradle](#with-gradle)
-
- + [MicroProfile TCK's](#microprofile-tck-s)
- + [Test Coverage](#test-coverage)
-* [Extensions](#extensions)
- + [Descriptions](#descriptions)
- + [Update dependencies to extensions](#update-dependencies-to-extensions)
- + [Check security vulnerabilities](#check-security-vulnerabilities)
-* [The small print](#the-small-print)
-* [Frequently Asked Questions](#frequently-asked-questions)
-
-Table of contents generated with
-markdown-toc
+- [Legal](#legal)
+- [Reporting an issue](#reporting-an-issue)
+- [Checking an issue is fixed in main](#checking-an-issue-is-fixed-in-main)
+ * [Using snapshots](#using-snapshots)
+ * [Building main](#building-main)
+ * [Updating the version](#updating-the-version)
+- [Before you contribute](#before-you-contribute)
+ * [Code reviews](#code-reviews)
+ * [Coding Guidelines](#coding-guidelines)
+ * [Continuous Integration](#continuous-integration)
+ * [Tests and documentation are not optional](#tests-and-documentation-are-not-optional)
+- [Setup](#setup)
+ * [IDE Config and Code Style](#ide-config-and-code-style)
+ + [Eclipse Setup](#eclipse-setup)
+ + [IDEA Setup](#idea-setup)
+ - [How to work](#how-to-work)
+ - [`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)
+ + [Building a single module of an extension](#building-a-single-module-of-an-extension)
+ + [Building with relocations](#building-with-relocations)
+ + [Running a single test](#running-a-single-test)
+ - [Maven Invoker tests](#maven-invoker-tests)
+ * [Build with multiple threads](#build-with-multiple-threads)
+ * [Don't build any test modules](#don-t-build-any-test-modules)
+ + [Automatic incremental build](#automatic-incremental-build)
+ - [Special case `bom-descriptor-json`](#special-case--bom-descriptor-json-)
+ - [Usage by CI](#usage-by-ci)
+ - [Develocity build cache](#develocity-build-cache)
+- [Release your own version](#release-your-own-version)
+- [Documentation](#documentation)
+ * [Building the documentation](#building-the-documentation)
+ * [Referencing a new guide in the index](#referencing-a-new-guide-in-the-index)
+- [Usage](#usage)
+ + [With Maven](#with-maven)
+ + [With Gradle](#with-gradle)
+ * [MicroProfile TCK's](#microprofile-tck-s)
+ * [Test Coverage](#test-coverage)
+- [Extensions](#extensions)
+ * [Descriptions](#descriptions)
+ * [Update dependencies to extensions](#update-dependencies-to-extensions)
+ * [Check security vulnerabilities](#check-security-vulnerabilities)
+- [The small print](#the-small-print)
+- [Frequently Asked Questions](#frequently-asked-questions)
+
+Table of contents generated with markdown-toc
## Legal
@@ -531,21 +530,74 @@ CI is using a slightly different GIB config than locally:
For more details see the `Get GIB arguments` step in `.github/workflows/ci-actions-incremental.yml`.
-##### Gradle Enterprise build cache
+##### Develocity build cache
-Quarkus has a Gradle Enterprise setup at https://ge.quarkus.io that can be used to analyze the build performance of the Quarkus project.
+###### Getting set up
-Locally you can use `-Dgradle.cache.local.enabled=true` to enable the local Gradle Enterprise cache. This can speed up the build significantly. It is still considered experimental but can be used for local development.
+Quarkus has a Develocity instance set up at https://ge.quarkus.io that can be used to analyze the build performance of the Quarkus project and also provides build cache services.
-If you have a need or interest to report build times, you will need to get an API key for the GE instance. It is mainly relevant for those working on optimizing the Quarkus build. Ping on quarkus-dev mailing list or on Zulip if you need one.
+If you have an account on https://ge.quarkus.io, this can speed up your local builds significantly.
-When you have the account setup you run `mvn gradle-enterprise:provision-access-key` and login - from then on build time info will be sent to the GE instance.
-You can alternatively also generate an API key from the GE UI and then use an environment variable like this:
+If you have a need or interest to share your build scans and use the build cache, you will need to get an account for the Develocity instance.
+It is only relevant for members of the Quarkus team and you should contact either Guillaume Smet or Max Andersen to set up your account.
+
+When you have the account set up, from the root of your local Quarkus workspace, run:
+
+```
+./mvnw gradle-enterprise:provision-access-key
+```
+
+and log in in the browser window it will open (if not already logged in).
+Your access key will be stored in the `~/.m2/.gradle-enterprise/keys.properties` file.
+From then your build scans will be sent to the Develocity instance and you will be able to benefit from the build cache.
+
+You can alternatively also generate an API key from the Develocity UI and then use an environment variable like this:
```
export GRADLE_ENTERPRISE_ACCESS_KEY=ge.quarkus.io=a_secret_key
```
+When debugging a test (and especially flaky tests), you might want to temporarily disable the build cache.
+You can easily do it by adding `-Dno-build-cache` to your Maven command.
+
+The remote cache is stored on the Develocity server and is populated by CI.
+To be able to benefit from the remote cache, you need to use a Java version tested on CI (at the moment, either 17 or 21) and the same Maven version (thus why it is recommended to use the Maven wrapper aka `./mvnw`).
+Note that the local cache alone should bring you a significant speedup.
+
+The local cache is stored in the `~/.m2/.gradle-enterprise/build-cache/` directory.
+If you have problems with your local cache, you can delete this directory.
+
+###### -Dquickly
+
+When using `-Dquickly` with no goals, Develocity is unable to detect that the `clean` goal is present.
+We worked around it but you will get the following warnings at the beginning of your build output:
+
+```
+[WARNING] Build cache entries produced by this build may be incorrect since the clean lifecycle is not part of the build invocation.
+[WARNING] You must only invoke the build without the clean lifecycle if the build is started from a clean working directory.
+```
+
+You can safely ignore them.
+
+###### Benchmarking the build
+
+During the experiment phase, there might be a need to benchmark the build in a reliable manner.
+
+For this, we can use the [Gradle Profiler](https://github.com/gradle/gradle-profiler).
+It can be installed with SDKMAN! (`sdk install gradleprofiler`) or Homebrew (`brew install gradle-profiler`).
+
+Then we can run the following commands at the root of the Quarkus project:
+
+```
+# Without cache
+gradle-profiler --maven --benchmark --scenario-file build.scenario clean_install_no_cache
+
+# With cache
+gradle-profiler --maven --benchmark --scenario-file build.scenario clean_install
+```
+
+Simple HTML reports will be published in the `profile_out*` directories.
+
## Release your own version
You might want to release your own patched version of Quarkus to an internal repository.
diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 645b3af610c91f..2d11ea03f1d0a7 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -14,7 +14,7 @@
pom
- 2.0.1
+ 2.0.21.771.0.2.41.0.18
@@ -25,7 +25,7 @@
11.1.52.1.5.Final
- 3.1.1.Final
+ 3.1.2.Final6.2.7.Final0.33.00.2.4
@@ -34,8 +34,8 @@
1.32.01.32.0-alpha1.21.0-alpha
- 5.1.0.Final
- 1.12.2
+ 5.2.0.Final
+ 1.12.32.1.120.22.021.1
@@ -50,21 +50,21 @@
2.12.03.1.1
- 2.2.0
- 3.5.4
+ 2.3.0
+ 3.6.04.1.04.0.0
- 3.9.0
+ 3.10.02.7.06.2.64.4.02.1.01.0.133.0.1
- 3.8.0
- 4.16.0
+ 3.10.0
+ 4.18.02.5.0
- 2.1.2
+ 2.1.32.1.13.0.02.1.0
@@ -94,23 +94,23 @@
2.16.11.0.0.Final3.14.0
- 1.16.0
+ 1.16.11.7.0
- 6.4.3.Final
- 1.14.7
+ 6.4.4.Final
+ 1.14.116.0.6.Final2.2.2.Final8.0.1.Final
- 7.0.0.Final
+ 7.1.0.Final7.0.0.Final
- 2.1
+ 2.38.0.0.Final
- 8.12.1
+ 8.12.22.2.212.2.5.Final2.2.2.Final
@@ -119,17 +119,17 @@
2.0.0.Final1.7.0.Final1.0.1.Final
- 2.2.3.Final
+ 2.3.1.Final3.5.1.Final
- 4.5.3
+ 4.5.44.5.144.4.164.1.59.2.12.3.22.2.224
- 42.7.1
- 3.3.2
+ 42.7.2
+ 3.3.38.3.012.4.2.jre111.6.7
@@ -140,28 +140,27 @@
5.4.02.25.10.2
- 1.5.0
- 14.0.24.Final
- 4.6.5.Final
+ 15.0.0.CR1
+ 5.0.0.CR23.1.5
- 4.1.106.Final
- 1.12.0
+ 4.1.107.Final
+ 1.14.01.0.43.5.3.Final
- 2.5.6
- 3.6.1
+ 2.5.7
+ 3.7.01.8.01.1.10.50.100.0
- 2.13.12
+ 2.13.131.2.33.11.3
- 2.15.0
+ 2.15.12.2.01.0.01.9.22
- 1.7.3
+ 1.8.00.27.01.6.24.1.2
@@ -171,7 +170,7 @@
9.22.33.0.3
- 4.25.1
+ 4.26.04.24.02.26.0.0
@@ -189,26 +188,26 @@
5.8.04.13.02.0.3.Final
- 23.0.4
+ 23.0.71.15.13.42.0
- 2.24.1
- 0.25.0
- 1.43.3
+ 2.25.0
+ 0.26.0
+ 1.44.12.14.7.51.1.0
- 1.25.0
+ 1.26.01.11.02.10.11.1.2.Final
- 2.22.1
+ 2.23.01.3.0.Final1.11.32.5.8.Final0.1.18.Final
- 1.19.4
- 3.3.4
+ 1.19.6
+ 3.3.52.0.01.4.4
@@ -2075,6 +2074,16 @@
quarkus-websockets-client-deployment${project.version}
+
+ io.quarkus
+ quarkus-websockets-next
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-websockets-next-deployment
+ ${project.version}
+ io.quarkusquarkus-undertow-spi
@@ -5241,11 +5250,6 @@
elasticsearch-rest-client-sniffer${elasticsearch-opensource-components.version}
-
- org.junit-pioneer
- junit-pioneer
- ${junit-pioneer.version}
- org.jacocoorg.jacoco.core
@@ -5405,7 +5409,7 @@
org.infinispan
- infinispan-client-hotrod-jakarta
+ infinispan-client-hotrod${infinispan.version}
@@ -5448,7 +5452,7 @@
org.infinispan
- infinispan-commons-jakarta
+ infinispan-commons${infinispan.version}
diff --git a/bom/dev-ui/pom.xml b/bom/dev-ui/pom.xml
index be2f1cc71d1aee..677f6762fb5102 100644
--- a/bom/dev-ui/pom.xml
+++ b/bom/dev-ui/pom.xml
@@ -27,9 +27,9 @@
1.4.01.7.51.7.0
- 5.4.3
- 2.1.0
- 1.8.2
+ 5.5.0
+ 1.0.12
+ 1.8.32.4.02.15.3
@@ -262,12 +262,12 @@
- org.mvnpm.at.vanillawc
- wc-codemirror
- ${wc-codemirror.version}
+ org.mvnpm.at.mvnpm
+ codeblock
+ ${codeblock.version}runtime
-
+
org.mvnpm
diff --git a/build-parent/pom.xml b/build-parent/pom.xml
index 3659002a1b0856..1ca9aff872c000 100644
--- a/build-parent/pom.xml
+++ b/build-parent/pom.xml
@@ -75,7 +75,7 @@
and unfortunately annotation processors are not covered by dependency management
in kotlin-maven-plugin; see https://github.com/quarkusio/quarkus/issues/37477#issuecomment-1923662964
-->
- 6.4.3.Final
+ 6.4.4.Final4.13.0
@@ -83,19 +83,20 @@
:Z
- 8.9.1
+ 8.12.1docker.io/elastic/elasticsearch:${elasticsearch-server.version}docker.io/elastic/logstash:${elasticsearch-server.version}docker.io/elastic/kibana:${elasticsearch-server.version}http
- 2.9.0
+ 2.11.1docker.io/opensearchproject/opensearch:${opensearch-server.version}http
+ 2.2.0docker.io/postgres:14docker.io/mariadb:10.11
- docker.io/ibmcom/db2:11.5.7.0a
+ icr.io/db2_community/db2:11.5.9.0mcr.microsoft.com/mssql/server:2022-latestdocker.io/mysql:8.0docker.io/gvenzl/oracle-free:23-slim-faststart
@@ -106,7 +107,7 @@
- 23.0.4
+ 23.0.719.0.3quay.io/keycloak/keycloak:${keycloak.version}quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy
@@ -115,17 +116,17 @@
3.25.3
- 3.3.1
+ 3.4.17.3.0
- 2.31.2
+ 2.32.02.0.0
- 0.43.4
+ 0.44.02.23.01.9.03.6.0
@@ -160,8 +161,6 @@
sh${maven.multiModuleProjectDirectory}/.github/docker-prune.${script.extension}
- ${enforcer.skip}
-
${surefire.argLine.additional}1.7.0
@@ -291,6 +290,12 @@
${assertj.version}test
+
+ org.junit-pioneer
+ junit-pioneer
+ ${junit-pioneer.version}
+ test
+ org.asciidoctor
@@ -373,6 +378,13 @@
+
+ me.escoffier.certs
+ certificate-generator-junit5
+ 0.4.3
+ test
+
+
@@ -501,71 +513,21 @@
-
- classpath:enforcer-rules/quarkus-require-java-version.xml
-
-
- classpath:enforcer-rules/quarkus-require-maven-version.xml
- classpath:enforcer-rules/quarkus-banned-dependencies.xmlclasspath:enforcer-rules/quarkus-banned-dependencies-okhttp.xml
+
+ classpath:enforcer-rules/quarkus-banned-dependencies-test.xml
+ enforce
-
- enforce-test-deps-scope
-
-
-
-
- io.quarkus:quarkus-test-*
- io.rest-assured:*
- org.assertj:*
- junit:junit
-
-
- io.quarkus:quarkus-test-*:*:*:test
- io.rest-assured:*:*:*:test
- org.assertj:*:*:*:test
- junit:junit:*:*:test
-
- Found test dependencies with wrong scope:
-
-
- ${enforce-test-deps-scope.skip}
-
-
- enforce
-
-
-
- enforce-test-deps-junit-scope
-
-
-
- false
-
- org.junit.jupiter:*
-
-
- org.junit.jupiter:*:*:*:test
-
- Found JUnit dependencies with wrong scope:
-
-
- ${enforce-test-deps-scope.skip}
-
-
- enforce
-
-
@@ -712,7 +674,6 @@
org.apache.maven.pluginsmaven-plugin-plugin
- ${maven-plugin-plugin.version}true
diff --git a/build.scenario b/build.scenario
new file mode 100644
index 00000000000000..cc7794fb99ec7f
--- /dev/null
+++ b/build.scenario
@@ -0,0 +1,40 @@
+clean_install {
+ tasks = ["clean","install","-Dquickly", "-T6"]
+ cleanup-tasks = ["clean"]
+ maven {
+ targets = ["clean","install","-Dquickly", "-T6"]
+ }
+ warm-ups = 1
+ iterations = 3
+}
+
+clean_install_no_scan {
+ tasks = ["clean","install","-Dquickly", "-T6", "-Dscan=false"]
+ cleanup-tasks = ["clean"]
+ maven {
+ targets = ["clean","install","-Dquickly", "-T6", "-Dscan=false"]
+ }
+ warm-ups = 1
+ iterations = 3
+}
+
+clean_install_no_cache {
+ tasks = ["clean","install","-Dquickly", "-T6", "-Dno-build-cache"]
+ cleanup-tasks = ["clean"]
+ maven {
+ targets = ["clean","install","-Dquickly", "-T6", "-Dno-build-cache"]
+ }
+ warm-ups = 1
+ iterations = 3
+}
+
+clean_install_no_cache_no_scan {
+ tasks = ["clean","install","-Dquickly", "-T6", "-Dno-build-cache", "-Dscan=false"]
+ cleanup-tasks = ["clean"]
+ maven {
+ targets = ["clean","install","-Dquickly", "-T6", "-Dno-build-cache", "-Dscan=false"]
+ }
+ warm-ups = 1
+ iterations = 3
+}
+
diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml
index d4b13b09d9a8d3..9abc0fadd2204f 100644
--- a/core/deployment/pom.xml
+++ b/core/deployment/pom.xml
@@ -153,21 +153,30 @@
+ org.apache.maven.pluginsmaven-enforcer-plugin
- enforce-test-deps-junit-scope
-
- true
-
+ enforceenforce
-
-
- enforce-quarkus-core-deployment
-
+
+
+
+
+ classpath:enforcer-rules/quarkus-banned-dependencies.xml
+
+
+ classpath:enforcer-rules/quarkus-banned-dependencies-okhttp.xml
+
+
@@ -180,9 +189,6 @@
-
- enforce
-
@@ -214,30 +220,6 @@
-
-
-
- com.gradle
- gradle-enterprise-maven-extension
-
-
-
-
-
- maven-compiler-plugin
-
- the extension config doc generation tool shares data across all extensions
-
-
-
-
-
-
-
-
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java
index 12448f389fcd04..8f3e059be79f02 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java
@@ -24,6 +24,13 @@ public class BootstrapConfig {
@ConfigItem(defaultValue = "false")
Boolean workspaceDiscovery;
+ /**
+ * If set to true, workspace loader will log warnings for modules that could not be loaded for some reason
+ * instead of throwing errors.
+ */
+ @ConfigItem(defaultValue = "false")
+ boolean warnOnFailingWorkspaceModules;
+
/**
* By default, the bootstrap mechanism will create a shared cache of open JARs for
* Quarkus classloaders to reduce the total number of opened ZIP FileSystems in dev and test modes.
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java
index 77ed4fba53079b..bd13ee0931508e 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java
@@ -152,6 +152,49 @@ public String getSource() {
return source;
}
+ /**
+ * Creates a new {@link Builder} instance, using the specified class for the underlying {@link Type} which hierarchy is to
+ * be be registered for reflection.
+ *
+ * @param clazz the Class which hierarchy is to be registered for reflection
+ * @return a new {@link Builder} instance, initialized from the specified Class
+ */
+ public static Builder builder(Class> clazz) {
+ return builder(clazz.getName());
+ }
+
+ /**
+ * Creates a new {@link Builder} instance, using the specified class for the underlying {@link Type} which hierarchy is to
+ * be be registered for reflection.
+ *
+ * @param className the name of the Class which hierarchy is to be registered for reflection
+ * @return a new {@link Builder} instance, initialized from the specified Class
+ */
+ public static Builder builder(String className) {
+ return builder(DotName.createSimple(className));
+ }
+
+ /**
+ * Creates a new {@link Builder} instance, using the specified class for the underlying {@link Type} which hierarchy is to
+ * be be registered for reflection.
+ *
+ * @param className the {@link DotName} of the Class which hierarchy is to be registered for reflection
+ * @return a new {@link Builder} instance, initialized from the specified Class
+ */
+ public static Builder builder(DotName className) {
+ return builder(Type.create(className, Type.Kind.CLASS));
+ }
+
+ /**
+ * Creates a new {@link Builder} instance, initializing it with the specified {@link Type}
+ *
+ * @param type the {@link Type} which hierarchy is to be registered for reflection
+ * @return a new {@link Builder} instance, initialized from the specified {@link Type}
+ */
+ public static Builder builder(Type type) {
+ return new Builder().type(type);
+ }
+
public static class Builder {
private Type type;
@@ -167,6 +210,26 @@ public Builder type(Type type) {
return this;
}
+ /**
+ * Derives the target {@link Type} to be registered from the specified class name.
+ *
+ * @param className a {@link DotName} representing the name of the class of the Type to be registered for reflection
+ * @return this {@link Builder} instance
+ */
+ public Builder className(DotName className) {
+ return type(Type.create(className, Type.Kind.CLASS));
+ }
+
+ /**
+ * Derives the target {@link Type} to be registered from the specified class name.
+ *
+ * @param className the name of the class of the Type to be registered for reflection
+ * @return this {@link Builder} instance
+ */
+ public Builder className(String className) {
+ return className(DotName.createSimple(className));
+ }
+
public Builder index(IndexView index) {
this.index = index;
return this;
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
index 4266d04727a6eb..eab173ac613a86 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
@@ -926,9 +926,11 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB
}
}
if (removedFromThisArchive.isEmpty()) {
- Files.copy(resolvedDep, targetPath, StandardCopyOption.REPLACE_EXISTING);
+ Files.copy(resolvedDep, targetPath, StandardCopyOption.REPLACE_EXISTING,
+ StandardCopyOption.COPY_ATTRIBUTES);
} else {
- //we have removed classes, we need to handle them correctly
+ // we copy jars for which we remove entries to the same directory
+ // which seems a bit odd to me
filterZipFile(resolvedDep, targetPath, removedFromThisArchive);
}
}
@@ -1251,6 +1253,8 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf
}
}
}
+ // let's make sure we keep the original timestamp
+ Files.setLastModifiedTime(targetPath, Files.getLastModifiedTime(resolvedDep));
}
} catch (IOException e) {
throw new RuntimeException(e);
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 7ab2f19910dd10..237e18cafa3445 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
@@ -7,6 +7,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -15,7 +16,6 @@
import org.jboss.logging.Logger;
import io.quarkus.deployment.annotations.BuildProducer;
-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.NativeImageBuildItem;
@@ -34,7 +34,6 @@ public class UpxCompressionBuildStep {
*/
private static final String PATH = "PATH";
- @BuildStep(onlyIf = NativeBuild.class)
public void compress(NativeConfig nativeConfig, NativeImageRunnerBuildItem nativeImageRunner,
NativeImageBuildItem image,
BuildProducer upxCompressedProducer,
@@ -70,11 +69,13 @@ public void compress(NativeConfig nativeConfig, NativeImageRunnerBuildItem nativ
}
private boolean runUpxFromHost(File upx, File executable, NativeConfig nativeConfig) {
- String level = getCompressionLevel(nativeConfig.compression().level().getAsInt());
List extraArgs = nativeConfig.compression().additionalArgs().orElse(Collections.emptyList());
- List args = Stream.concat(
- Stream.concat(Stream.of(upx.getAbsolutePath(), level), extraArgs.stream()),
+ List args = Stream.of(
+ Stream.of(upx.getAbsolutePath()),
+ nativeConfig.compression().level().stream().mapToObj(this::getCompressionLevel),
+ extraArgs.stream(),
Stream.of(executable.getAbsolutePath()))
+ .flatMap(Function.identity())
.collect(Collectors.toList());
log.infof("Executing %s", String.join(" ", args));
final ProcessBuilder processBuilder = new ProcessBuilder(args)
@@ -104,7 +105,6 @@ private boolean runUpxFromHost(File upx, File executable, NativeConfig nativeCon
private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig nativeConfig,
String effectiveBuilderImage) {
- String level = getCompressionLevel(nativeConfig.compression().level().getAsInt());
List extraArgs = nativeConfig.compression().additionalArgs().orElse(Collections.emptyList());
List commandLine = new ArrayList<>();
@@ -140,7 +140,9 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig
volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + ":z");
commandLine.add(effectiveBuilderImage);
- commandLine.add(level);
+ if (nativeConfig.compression().level().isPresent()) {
+ commandLine.add(getCompressionLevel(nativeConfig.compression().level().getAsInt()));
+ }
commandLine.addAll(extraArgs);
commandLine.add(nativeImage.getPath().toFile().getName());
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java
index 264350bb54b5c2..197145c7bfc0a2 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java
@@ -45,17 +45,18 @@ public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities ca
ReflectiveHierarchyBuildItem.Builder builder = new ReflectiveHierarchyBuildItem.Builder();
Set processedReflectiveHierarchies = new HashSet();
+ IndexView index = combinedIndexBuildItem.getComputingIndex();
for (AnnotationInstance i : combinedIndexBuildItem.getIndex()
.getAnnotations(DotName.createSimple(RegisterForReflection.class.getName()))) {
- boolean methods = getBooleanValue(i, "methods");
- boolean fields = getBooleanValue(i, "fields");
- boolean ignoreNested = getBooleanValue(i, "ignoreNested");
- boolean serialization = i.value("serialization") != null && i.value("serialization").asBoolean();
- boolean unsafeAllocated = i.value("unsafeAllocated") != null && i.value("unsafeAllocated").asBoolean();
+ boolean methods = i.valueWithDefault(index, "methods").asBoolean();
+ boolean fields = i.valueWithDefault(index, "fields").asBoolean();
+ boolean ignoreNested = i.valueWithDefault(index, "ignoreNested").asBoolean();
+ boolean serialization = i.valueWithDefault(index, "serialization").asBoolean();
+ boolean unsafeAllocated = i.valueWithDefault(index, "unsafeAllocated").asBoolean();
+ boolean registerFullHierarchyValue = i.valueWithDefault(index, "registerFullHierarchy").asBoolean();
AnnotationValue targetsValue = i.value("targets");
- AnnotationValue registerFullHierarchyValue = i.value("registerFullHierarchy");
AnnotationValue classNamesValue = i.value("classNames");
AnnotationValue lambdaCapturingTypesValue = i.value("lambdaCapturingTypes");
@@ -114,14 +115,14 @@ private void registerClass(ClassLoader classLoader, String className, boolean me
boolean ignoreNested, boolean serialization, boolean unsafeAllocated,
final BuildProducer reflectiveClass,
BuildProducer reflectiveClassHierarchy, Set processedReflectiveHierarchies,
- AnnotationValue registerFullHierarchyValue, Builder builder) {
+ boolean registerFullHierarchyValue, Builder builder) {
reflectiveClass.produce(serialization
? ReflectiveClassBuildItem.builder(className).serialization().unsafeAllocated(unsafeAllocated).build()
: ReflectiveClassBuildItem.builder(className).constructors().methods(methods).fields(fields)
.unsafeAllocated(unsafeAllocated).build());
//Search all class hierarchy, fields and methods in order to register its classes for reflection
- if (registerFullHierarchyValue != null && registerFullHierarchyValue.asBoolean()) {
+ if (registerFullHierarchyValue) {
registerClassDependencies(reflectiveClassHierarchy, classLoader, processedReflectiveHierarchies, methods, builder,
className);
}
@@ -228,7 +229,4 @@ private static Type getMethodReturnType(IndexView indexView, DotName initialName
return methodReturnType;
}
- private static boolean getBooleanValue(AnnotationInstance i, String name) {
- return i.value(name) == null || i.value(name).asBoolean();
- }
}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ProvidedExtensionDepsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ProvidedExtensionDepsTest.java
index adaca8f5ead223..8a259cd7269469 100644
--- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ProvidedExtensionDepsTest.java
+++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ProvidedExtensionDepsTest.java
@@ -50,6 +50,7 @@ protected TsArtifact composeApplication() {
final TsArtifact depC2 = TsArtifact.jar("dep-c", "2");
// make sure provided dependencies don't override compile/runtime dependencies
directProvidedDep.addDependency(depC2);
+ directProvidedDep.addDependency(extADeploymentDep);
final TsArtifact transitiveProvidedDep = TsArtifact.jar("transitive-provided-dep");
directProvidedDep.addDependency(transitiveProvidedDep);
@@ -68,7 +69,7 @@ protected void assertAppModel(ApplicationModel model) throws Exception {
expected.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-deployment", "1"),
DependencyFlags.DEPLOYMENT_CP));
expected.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-deployment-dep", "1"),
- DependencyFlags.DEPLOYMENT_CP));
+ DependencyFlags.DEPLOYMENT_CP, DependencyFlags.COMPILE_ONLY));
assertEquals(expected, getDeploymentOnlyDeps(model));
final Set expectedRuntime = new HashSet<>();
@@ -83,7 +84,8 @@ protected void assertAppModel(ApplicationModel model) throws Exception {
DependencyFlags.DEPLOYMENT_CP));
expectedRuntime.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "dep-c", "1"),
DependencyFlags.RUNTIME_CP,
- DependencyFlags.DEPLOYMENT_CP));
+ DependencyFlags.DEPLOYMENT_CP,
+ DependencyFlags.COMPILE_ONLY));
assertEquals(expectedRuntime, getDependenciesWithFlag(model, DependencyFlags.RUNTIME_CP));
final Set expectedCompileOnly = new HashSet<>();
@@ -102,6 +104,13 @@ protected void assertAppModel(ApplicationModel model) throws Exception {
.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "transitive-provided-dep", "1"),
JavaScopes.PROVIDED,
DependencyFlags.COMPILE_ONLY));
+ expectedCompileOnly.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "dep-c", "1"),
+ DependencyFlags.RUNTIME_CP,
+ DependencyFlags.DEPLOYMENT_CP,
+ DependencyFlags.COMPILE_ONLY));
+ expectedCompileOnly
+ .add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-deployment-dep", "1"),
+ DependencyFlags.DEPLOYMENT_CP, DependencyFlags.COMPILE_ONLY));
assertEquals(expectedCompileOnly, getDependenciesWithFlag(model, DependencyFlags.COMPILE_ONLY));
final Set compileOnlyPlusRuntime = new HashSet<>();
diff --git a/core/launcher/pom.xml b/core/launcher/pom.xml
index e9d538d20e0560..c549657617861a 100644
--- a/core/launcher/pom.xml
+++ b/core/launcher/pom.xml
@@ -92,7 +92,6 @@
org.apache.maven.pluginsmaven-jar-plugin
- 2023-10-17T10:15:30Z**/LauncherShader.class
diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java
index f5e3720291c584..7e550d58ad3ffb 100644
--- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java
+++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java
@@ -2,6 +2,7 @@
import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_DOC_DEFAULT;
import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_DOC_ENUM_VALUE;
+import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_DOC_IGNORE;
import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_DOC_MAP_KEY;
import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_DOC_SECTION;
import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_ITEM;
@@ -246,6 +247,8 @@ private List recursivelyFindConfigItems(Element element, String r
: annotationMirror.getElementValues().values().iterator().next().getValue().toString();
} else if (annotationName.equals(ANNOTATION_CONFIG_WITH_UNNAMED_KEY)) {
unnamedMapKey = true;
+ } else if (annotationName.equals(ANNOTATION_CONFIG_DOC_IGNORE)) {
+ generateDocumentation = false;
}
}
@@ -390,7 +393,8 @@ private List recursivelyFindConfigItems(Element element, String r
configDocKey.setConfigPhase(configPhase);
configDocKey.setDefaultValue(defaultValue);
configDocKey.setDocMapKey(configDocMapKey);
- configDocKey.setConfigDoc(javaDocParser.parseConfigDescription(rawJavaDoc));
+ javaDocParser.parseConfigDescription(rawJavaDoc, configDocKey::setConfigDoc, configDocKey::setSince);
+ configDocKey.setEnvironmentVariable(DocGeneratorUtil.toEnvVarName(name));
configDocKey.setAcceptedValues(acceptedValues);
configDocKey.setJavaDocSiteLink(getJavaDocSiteLink(type));
ConfigDocItem configDocItem = new ConfigDocItem();
@@ -628,6 +632,8 @@ private List decorateGroupItems(
additionalKeys.addAll(additionalNames.stream().map(k -> k + configDocKey.getKey()).collect(toList()));
configDocKey.setAdditionalKeys(additionalKeys);
configDocKey.setKey(parentName + configDocKey.getKey());
+ configDocKey.setEnvironmentVariable(
+ DocGeneratorUtil.toEnvVarName(parentName) + configDocKey.getEnvironmentVariable());
decoratedItems.add(configDocItem);
} else {
ConfigDocSection section = configDocItem.getConfigDocSection();
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 a853a0f92939ec..f4044599f84411 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
@@ -27,6 +27,8 @@ final public class ConfigDocKey implements ConfigDocElement, Comparable ref = new AtomicReference<>();
+ parseConfigDescription(javadocComment, ref::set, s -> {
+ });
+ return ref.get();
+ }
+
+ public void parseConfigDescription(
+ String javadocComment,
+ Consumer javadocTextConsumer,
+ Consumer sinceConsumer) {
+
if (javadocComment == null || javadocComment.trim().isEmpty()) {
- return Constants.EMPTY;
+ javadocTextConsumer.accept(Constants.EMPTY);
+ return;
}
// the parser expects all the lines to start with "* "
@@ -90,10 +105,16 @@ public String parseConfigDescription(String javadocComment) {
Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment);
if (isAsciidoc(javadoc)) {
- return handleEolInAsciidoc(javadoc);
+ javadocTextConsumer.accept(handleEolInAsciidoc(javadoc));
+ } else {
+ javadocTextConsumer.accept(htmlJavadocToAsciidoc(javadoc.getDescription()));
}
-
- return htmlJavadocToAsciidoc(javadoc.getDescription());
+ javadoc.getBlockTags().stream()
+ .filter(t -> t.getType() == Type.SINCE)
+ .map(JavadocBlockTag::getContent)
+ .map(JavadocDescription::toText)
+ .findFirst()
+ .ifPresent(sinceConsumer::accept);
}
public SectionHolder parseConfigSection(String javadocComment, int sectionLevel) {
diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/MavenConfigDocBuilder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/MavenConfigDocBuilder.java
index 16d3c31a232052..a5b1fcf98cc0e6 100644
--- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/MavenConfigDocBuilder.java
+++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/MavenConfigDocBuilder.java
@@ -79,11 +79,8 @@ public void addParam(String type, String name, String defaultValue, boolean requ
configDocKey.setAdditionalKeys(List.of(name));
configDocKey.setConfigPhase(ConfigPhase.RUN_TIME);
configDocKey.setDefaultValue(defaultValue == null ? Constants.EMPTY : defaultValue);
- if (description != null && !description.isBlank()) {
- configDocKey.setConfigDoc(javaDocParser.parseConfigDescription(description));
- } else {
- configDocKey.setConfigDoc(EMPTY);
- }
+ javaDocParser.parseConfigDescription(description, configDocKey::setConfigDoc, configDocKey::setSince);
+ configDocKey.setEnvironmentVariable(DocGeneratorUtil.toEnvVarName(name));
configDocKey.setOptional(!required);
final ConfigDocItem configDocItem = new ConfigDocItem();
configDocItem.setConfigDocKey(configDocKey);
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 f23705f230e63e..60d4200fe52a36 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
@@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
import org.asciidoctor.Asciidoctor.Factory;
import org.junit.jupiter.api.BeforeEach;
@@ -252,6 +253,15 @@ public void parseJavaDocWithCodeBlock() {
// parser.parseConfigDescription("Example:\n\n
{@code\nfoo\nbar\n}
"));
}
+ @Test
+ public void since() {
+ AtomicReference javadoc = new AtomicReference<>();
+ AtomicReference since = new AtomicReference<>();
+ parser.parseConfigDescription("Javadoc text\n\n@since 1.2.3", javadoc::set, since::set);
+ assertEquals("Javadoc text", javadoc.get());
+ assertEquals("1.2.3", since.get());
+ }
+
@Test
public void asciidoc() {
String asciidoc = "== My Asciidoc\n" +
diff --git a/core/runtime/pom.xml b/core/runtime/pom.xml
index f7cde7edbb5b3b..0fd1e0e2a6a10a 100644
--- a/core/runtime/pom.xml
+++ b/core/runtime/pom.xml
@@ -272,30 +272,6 @@
-
-
-
- com.gradle
- gradle-enterprise-maven-extension
-
-
-
-
-
- maven-compiler-plugin
-
- the extension config doc generation tool shares data across all extensions
-
-
-
-
-
-
-
-
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/RegisterForReflection.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RegisterForReflection.java
index 8cf279a84f64b1..f08da15fcdd9be 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/annotations/RegisterForReflection.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RegisterForReflection.java
@@ -7,9 +7,12 @@
/**
* Annotation that can be used to force a class to be registered for reflection in native image mode.
- * Note that by default nested classes and interfaces are not registered, unless {@link #ignoreNested()} is set to false.
- * Similarly, by default only the class itself is registered, not the full class hierarchy. This can be changed by setting
- * {@link #registerFullHierarchy()} to true.
+ * Note that by default the class itself is registered including nested classes and interfaces,
+ * but not the full class hierarchy. This can be changed by setting:
+ *
+ *
{@link #ignoreNested()} to true, to ignore nested classes.
+ *
{@link #registerFullHierarchy()} to true, to register the full hierarchy.
+ *
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@@ -26,11 +29,10 @@
boolean fields() default true;
/**
- * If nested classes/interfaces should be ignored/registered
- *
- * This is useful when it's necessary to register inner (especially private) classes for Reflection.
+ * If nested classes/interfaces should be ignored.
+ * By default, nested classes are registered. To ignore them set it to true.
*/
- boolean ignoreNested() default true;
+ boolean ignoreNested() default false;
/**
* Alternative classes that should actually be registered for reflection instead of the current class.
diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml
index bdc9ab6b66b464..dcc5e857a1f9c5 100644
--- a/devtools/bom-descriptor-json/pom.xml
+++ b/devtools/bom-descriptor-json/pom.xml
@@ -2891,6 +2891,19 @@
+
+ io.quarkus
+ quarkus-websockets-next
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
diff --git a/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java b/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java
index 82fd27e2c7542e..e2a01748c60054 100644
--- a/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java
+++ b/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java
@@ -101,6 +101,7 @@ public int run(String... args) throws Exception {
//When running tests the cli should not prompt for user input.
boolean interactiveMode = Arrays.stream(args).noneMatch(arg -> arg.equals("--cli-test"));
Optional testDir = Arrays.stream(args).dropWhile(arg -> !arg.equals("--cli-test-dir")).skip(1).findFirst();
+ boolean noCommand = args.length == 0 || args[0].startsWith("-");
boolean helpCommand = Arrays.stream(args).anyMatch(arg -> arg.equals("--help"));
boolean pluginCommand = args.length >= 1 && (args[0].equals("plug") || args[0].equals("plugin"));
@@ -111,7 +112,7 @@ public int run(String... args) throws Exception {
// If the command already exists and is not a help command (that lists subcommands) or plugin command, then just execute
// without dealing with plugins.
// The reason that we check if its a plugin command is that plugin commands need PluginManager initialization.
- if (existingCommand && !helpCommand && !pluginCommand) {
+ if (existingCommand && !noCommand && !helpCommand && !pluginCommand) {
return cmd.execute(args);
}
PluginCommandFactory pluginCommandFactory = new PluginCommandFactory(output);
@@ -119,14 +120,15 @@ public int run(String... args) throws Exception {
pluginManager.syncIfNeeded();
Map plugins = new HashMap<>(pluginManager.getInstalledPlugins());
pluginCommandFactory.populateCommands(cmd, plugins);
- missingCommand.ifPresent(m -> {
+ missingCommand.filter(m -> !plugins.containsKey(m)).ifPresent(m -> {
try {
+ output.info("Command %s is not available, looking for available plugins ...", m);
Map installable = pluginManager.getInstallablePlugins();
if (installable.containsKey(m)) {
Plugin candidate = installable.get(m);
PluginListItem item = new PluginListItem(false, candidate);
PluginListTable table = new PluginListTable(List.of(item));
- output.info("Command %s not installed but the following plugin is available:\n%s", m,
+ output.info("Plugin %s is available:\n%s", m,
table.getContent());
if (interactiveMode && Prompt.yesOrNo(true,
"Would you like to install it now?",
diff --git a/devtools/cli/src/main/java/io/quarkus/cli/core/Reflections.java b/devtools/cli/src/main/java/io/quarkus/cli/core/Reflections.java
index 2fe0dffa125d36..b3e6eb35849de0 100644
--- a/devtools/cli/src/main/java/io/quarkus/cli/core/Reflections.java
+++ b/devtools/cli/src/main/java/io/quarkus/cli/core/Reflections.java
@@ -39,6 +39,6 @@
org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory.class,
org.eclipse.aether.internal.impl.collect.DefaultDependencyCollector.class,
org.eclipse.aether.transport.wagon.WagonTransporterFactory.class
-})
+}, ignoreNested = true)
public class Reflections {
}
diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java
index 6f7df0aad8e79c..718a53f8798f94 100644
--- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java
+++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java
@@ -1,5 +1,7 @@
package io.quarkus.gradle.extension;
+import static io.quarkus.runtime.LaunchMode.*;
+
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -38,6 +40,7 @@
import io.quarkus.gradle.tasks.QuarkusGradleUtils;
import io.quarkus.gradle.tooling.ToolingUtils;
import io.quarkus.runtime.LaunchMode;
+import io.smallrye.config.SmallRyeConfig;
public abstract class QuarkusPluginExtension extends AbstractQuarkusExtension {
private final SourceSetExtension sourceSetExtension;
@@ -67,10 +70,14 @@ public void manifest(Action action) {
public void beforeTest(Test task) {
try {
- final Map props = task.getSystemProperties();
+ Map props = task.getSystemProperties();
+ ApplicationModel appModel = getApplicationModel(TEST);
+
+ SmallRyeConfig config = buildEffectiveConfiguration(appModel.getAppArtifact()).getConfig();
+ config.getOptionalValue(TEST.getProfileKey(), String.class)
+ .ifPresent(value -> props.put(TEST.getProfileKey(), value));
- final ApplicationModel appModel = getApplicationModel(LaunchMode.TEST);
- final Path serializedModel = ToolingUtils.serializeAppModel(appModel, task, true);
+ Path serializedModel = ToolingUtils.serializeAppModel(appModel, task, true);
props.put(BootstrapConstants.SERIALIZED_TEST_APP_MODEL, serializedModel.toString());
StringJoiner outputSourcesDir = new StringJoiner(",");
@@ -79,10 +86,10 @@ public void beforeTest(Test task) {
}
props.put(BootstrapConstants.OUTPUT_SOURCES_DIR, outputSourcesDir.toString());
- final SourceSetContainer sourceSets = getSourceSets();
- final SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
+ SourceSetContainer sourceSets = getSourceSets();
+ SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
- final File outputDirectoryAsFile = getLastFile(mainSourceSet.getOutput().getClassesDirs());
+ File outputDirectoryAsFile = getLastFile(mainSourceSet.getOutput().getClassesDirs());
Path projectDirPath = projectDir.toPath();
@@ -167,7 +174,7 @@ public Set combinedOutputSourceDirs() {
}
public AppModelResolver getAppModelResolver() {
- return getAppModelResolver(LaunchMode.NORMAL);
+ return getAppModelResolver(NORMAL);
}
public AppModelResolver getAppModelResolver(LaunchMode mode) {
@@ -175,7 +182,7 @@ public AppModelResolver getAppModelResolver(LaunchMode mode) {
}
public ApplicationModel getApplicationModel() {
- return getApplicationModel(LaunchMode.NORMAL);
+ return getApplicationModel(NORMAL);
}
public ApplicationModel getApplicationModel(LaunchMode mode) {
diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java
index f2696a9266f233..85f67e430dfc16 100644
--- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java
+++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java
@@ -1,12 +1,17 @@
package io.quarkus.gradle.tasks;
import static io.quarkus.gradle.tasks.QuarkusGradleUtils.getSourceSet;
+import static io.smallrye.common.expression.Expression.Flag.DOUBLE_COLON;
+import static io.smallrye.common.expression.Expression.Flag.LENIENT_SYNTAX;
+import static io.smallrye.common.expression.Expression.Flag.NO_SMART_BRACES;
+import static io.smallrye.common.expression.Expression.Flag.NO_TRIM;
import static java.util.Collections.emptyList;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -24,6 +29,7 @@
import io.quarkus.gradle.dsl.Manifest;
import io.quarkus.maven.dependency.ResolvedDependency;
+import io.smallrye.common.expression.Expression;
/**
* This base class exists to hide internal properties, make those only available in the {@link io.quarkus.gradle.tasks}
@@ -138,7 +144,7 @@ private EffectiveConfig buildEffectiveConfiguration(Map properti
* @param appArtifact the application dependency to retrive the quarkus application name and version.
* @return a filtered view of the configuration only with quarkus. names.
*/
- protected Map buildSystemProperties(ResolvedDependency appArtifact) {
+ protected Map buildSystemProperties(ResolvedDependency appArtifact, Map quarkusProperties) {
Map buildSystemProperties = new HashMap<>();
buildSystemProperties.putIfAbsent("quarkus.application.name", appArtifact.getArtifactId());
buildSystemProperties.putIfAbsent("quarkus.application.version", appArtifact.getVersion());
@@ -158,6 +164,33 @@ protected Map buildSystemProperties(ResolvedDependency appArtifa
buildSystemProperties.put(entry.getKey(), entry.getValue().toString());
}
}
+
+ Set quarkusValues = new HashSet<>();
+ quarkusValues.addAll(quarkusProperties.values());
+ quarkusValues.addAll(buildSystemProperties.values());
+
+ for (String value : quarkusValues) {
+ Expression expression = Expression.compile(value, LENIENT_SYNTAX, NO_TRIM, NO_SMART_BRACES, DOUBLE_COLON);
+ for (String reference : expression.getReferencedStrings()) {
+ String expanded = forcedPropertiesProperty.get().get(reference);
+ if (expanded != null) {
+ buildSystemProperties.put(reference, expanded);
+ continue;
+ }
+
+ expanded = quarkusBuildProperties.get().get(reference);
+ if (expanded != null) {
+ buildSystemProperties.put(reference, expanded);
+ continue;
+ }
+
+ expanded = (String) project.getProperties().get(reference);
+ if (expanded != null) {
+ buildSystemProperties.put(reference, expanded);
+ }
+ }
+ }
+
return buildSystemProperties;
}
diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/BaseConfig.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/BaseConfig.java
index 90c229c043b9b3..5e0d9533e92dcf 100644
--- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/BaseConfig.java
+++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/BaseConfig.java
@@ -21,20 +21,20 @@
final class BaseConfig {
private final Manifest manifest;
private final PackageConfig packageConfig;
- private final Map configMap;
+ private final Map values;
// Note: EffectiveConfig has all the code to load the configurations from all the sources.
BaseConfig(EffectiveConfig config) {
manifest = new Manifest();
packageConfig = new PackageConfig();
- ConfigInstantiator.handleObject(packageConfig, config.config());
+ ConfigInstantiator.handleObject(packageConfig, config.getConfig());
// populate the Gradle Manifest object
manifest.attributes(packageConfig.manifest.attributes);
packageConfig.manifest.manifestSections.forEach((section, attribs) -> manifest.attributes(attribs, section));
- configMap = config.configMap();
+ values = config.getValues();
}
PackageConfig packageConfig() {
@@ -53,7 +53,7 @@ Map cachingRelevantProperties(List propertyPatterns) {
List patterns = propertyPatterns.stream().map(s -> "^(" + s + ")$").map(Pattern::compile)
.collect(Collectors.toList());
Predicate> keyPredicate = e -> patterns.stream().anyMatch(p -> p.matcher(e.getKey()).matches());
- return configMap.entrySet().stream()
+ return values.entrySet().stream()
.filter(keyPredicate)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java
index 132d956279771e..d5b13e65e4a06c 100644
--- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java
+++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/Deploy.java
@@ -90,9 +90,8 @@ public Deploy() {
@TaskAction
public void checkRequiredExtensions() {
ApplicationModel appModel = resolveAppModelForBuild();
- Map configMap = extension().buildEffectiveConfiguration(appModel.getAppArtifact()).configMap();
Properties sysProps = new Properties();
- sysProps.putAll(configMap);
+ sysProps.putAll(extension().buildEffectiveConfiguration(appModel.getAppArtifact()).getValues());
try (CuratedApplication curatedApplication = QuarkusBootstrap.builder()
.setBaseClassLoader(getClass().getClassLoader())
.setExistingModel(appModel)
diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java
index 37cff3614d7795..1c67c52ac46379 100644
--- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java
+++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/EffectiveConfig.java
@@ -14,6 +14,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Supplier;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
@@ -26,6 +27,7 @@
import io.quarkus.runtime.configuration.ConfigUtils;
import io.smallrye.config.AbstractLocationConfigSourceLoader;
import io.smallrye.config.EnvConfigSource;
+import io.smallrye.config.Expressions;
import io.smallrye.config.PropertiesConfigSource;
import io.smallrye.config.PropertiesConfigSourceProvider;
import io.smallrye.config.SmallRyeConfig;
@@ -41,10 +43,9 @@
* Eventually used to construct a map with the effective config options from all the sources above and expose
* the Quarkus config objects like {@link PackageConfig}, {@link ClassLoadingConfig} and the underlying {@link SmallRyeConfig}.
*/
-final class EffectiveConfig {
- private final Map fullConfig;
-
+public final class EffectiveConfig {
private final SmallRyeConfig config;
+ private final Map values;
private EffectiveConfig(Builder builder) {
List configSources = new ArrayList<>();
@@ -81,13 +82,17 @@ private EffectiveConfig(Builder builder) {
.addAll(PropertiesConfigSourceProvider.classPathSources(META_INF_MICROPROFILE_CONFIG_PROPERTIES, classLoader));
this.config = buildConfig(builder.profile, configSources);
- this.fullConfig = generateFullConfigMap(config);
+ this.values = generateFullConfigMap(config);
}
- SmallRyeConfig config() {
+ public SmallRyeConfig getConfig() {
return config;
}
+ public Map getValues() {
+ return values;
+ }
+
private Map asStringMap(Map map) {
Map target = new HashMap<>();
map.forEach((k, v) -> {
@@ -100,14 +105,19 @@ private Map asStringMap(Map map) {
@VisibleForTesting
static Map generateFullConfigMap(SmallRyeConfig config) {
- Map map = new HashMap<>();
- config.getPropertyNames().forEach(property -> {
- String v = config.getConfigValue(property).getValue();
- if (v != null) {
- map.put(property, v);
+ return Expressions.withoutExpansion(new Supplier
+
+ io.quarkus
+ quarkus-websockets-next-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
@@ -2922,6 +2935,7 @@
copy-resources
+ ${skipDocs}${project.basedir}/target/asciidoc/sources
@@ -3111,6 +3125,7 @@
exec
+ ${skipDocs}java-classpath
@@ -3213,6 +3228,7 @@
single
+ ${skipDocs}assembly.xml
@@ -3243,31 +3259,51 @@
-
-
-
-
- com.gradle
- gradle-enterprise-maven-extension
-
-
-
-
-
- vale.dir
- git.dir
-
-
-
-
-
-
-
-
-
+
+ unbind-skip-docs
+
+
+ skipDocs
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+ none
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ default-jar
+ none
+
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+
+
+ default-install
+ none
+
+
+
+
+
+ documentation-pdf
@@ -3322,6 +3358,7 @@
single
+ ${skipDocs}assembly-pdf.xml
diff --git a/docs/src/main/asciidoc/amqp-reference.adoc b/docs/src/main/asciidoc/amqp-reference.adoc
index 85355b4b98e0dd..10cdff6374bae8 100644
--- a/docs/src/main/asciidoc/amqp-reference.adoc
+++ b/docs/src/main/asciidoc/amqp-reference.adoc
@@ -448,7 +448,7 @@ public AmqpClientOptions getNamedOptions() {
.setPemKeyCertOptions(keycert)
.setPemTrustOptions(trust)
.addEnabledSaslMechanism("EXTERNAL")
- .setHostnameVerificationAlgorithm("")
+ .setHostnameVerificationAlgorithm("") // Disables the hostname verification. Defaults is "HTTPS"
.setConnectTimeout(30000)
.setReconnectInterval(5000)
.setContainerId("my-container");
diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc
index 1980740a757c39..870980d16646fd 100644
--- a/docs/src/main/asciidoc/config-reference.adoc
+++ b/docs/src/main/asciidoc/config-reference.adoc
@@ -164,6 +164,26 @@ quarkus.http.port=9090 <2>
TIP: It works in the exact same way as Quarkus Application configuration file `application.properties`. Recommendation
is to use Quarkus `application.properties`.
+=== Locations
+
+Additionally to the default config locations, Quarkus provides a way to scan additional locations for configuration
+properties files.
+
+The `quarkus.config.locations` configuration property accepts multiple locations separated by a comma `,` and each
+must represent a valid `URI`. The supported `URI` schemes are:
+
+- file or directory (`file:`)
+- classpath resource
+- jar resource (`jar:`)
+- http resource (`http:`)
+
+All loaded sources use the same ordinal of the source that found the `quarkus.config.locations` configuration
+property. For instance, if `quarkus.config.locations` is set as a system property, then all loaded sources have their
+ordinals set to `400` (system properties use `400` as their ordinal). The ordinal may be overridden directly for each
+config source by setting the `config_ordinal` property and the ordinal value. The `config_ordinal` property only
+affects the ordinal of the source in which is being set. Sources are sorted first by their ordinal, then by location
+order, and finally by loading order.
+
=== Additional Config Sources
Quarkus provides additional extensions which cover other configuration formats and stores:
@@ -377,7 +397,12 @@ Properties in the profile aware file have priority over profile aware properties
[WARNING]
====
-The profile aware file must be present in the exact same location as the main `application.properties` file.
+Do not use profile aware files to set `quarkus.profile` or `quarkus.test.profile`. This will not work because the
+profile is required in advance to load the profile aware files.
+
+A profile aware file is only loaded if the unprofiled `application.properties` is also available in the same location
+and the file extension matches between the files. This is required to keep a consistent loading order and pair all the
+resources together.
====
=== Parent Profile
@@ -461,6 +486,12 @@ Then
* `my.prop` value is 5678.
* `another.prop` value is 1234.
+[WARNING]
+====
+Multiple profiles priority work in reverse order. With `quarkus.profile=common,dev`, Quarkus first checks the `dev`
+profile and then the `common` profile.
+====
+
=== Default Runtime Profile
The default Quarkus runtime profile is set to the profile used to build the application:
@@ -710,6 +741,11 @@ Maven projects could add the following goal to their `quarkus-maven-plugin` conf
The `track-config-changes` goal looks for `${project.basedir}/.quarkus/quarkus-prod-config-dump` (file name and directory are configurable) and, if the file already exists, checks whether the values stored in the config dump have changed.
It will log the changed options and save the current values of each of the options present in `${project.basedir}/.quarkus/quarkus-prod-config-dump` in `${project.basedir}/target/quarkus-prod-config.check` (the target file name and location can be configured). If the build time configuration has not changed since the last build both `${project.basedir}/.quarkus/quarkus-prod-config-dump` and `${project.basedir}/.quarkus/quarkus-prod-config-dump` will be identical.
+==== Dump Quarkus application dependencies
+
+In addition to dumping configuration values, `track-config-changes` goal also dumps all the Quarkus application dependencies, including Quarkus build time dependencies, along with their checksums (Adler32). This file could be used to check whether Quarkus build classpath has changed since the previous run.
+By default, the dependency checksums will be stored under `target/quarkus-prod-dependency-checksums.txt` file. A different location could be configured using plugin parameters.
+
==== Dump current build configuration when the recorded configuration isn't found
By default, `track-config-changes` looks for the configuration recorded during previous build and does nothing if it's not found. Enabling `dumpCurrentWhenRecordedUnavailable` parameter will make it dump the current build configuration
diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc
index 39c49c6c55d391..d8dff1bd729e2e 100644
--- a/docs/src/main/asciidoc/dev-services.adoc
+++ b/docs/src/main/asciidoc/dev-services.adoc
@@ -100,6 +100,12 @@ xref:rabbitmq-dev-services.adoc[RabbitMQ Dev Services Guide].
include::{generated-dir}/config/quarkus-smallrye-reactivemessaging-rabbitmq-config-group-rabbit-mq-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1]
+== Pulsar
+
+The Pulsar Dev Service will be enabled when the `quarkus-smallrye-reactive-messaging-pulsar` extension is present in your application, and
+the broker address has not been explicitly configured. More information can be found in the
+xref:pulsar-dev-services.adoc[Pulsar Dev Services Guide].
+
== Redis
The Redis Dev Service will be enabled when the `quarkus-redis-client` extension is present in your application, and
diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc
index 42454fba55d291..b7b2d52f966a84 100644
--- a/docs/src/main/asciidoc/dev-ui.adoc
+++ b/docs/src/main/asciidoc/dev-ui.adoc
@@ -684,7 +684,7 @@ image::dev-ui-qui-code-block-v2.png[alt=Dev UI Code Block,role="center"]
[source,javascript]
----
-import 'qui-code-block';
+import '@quarkus-webcomponents/codeblock';
----
[source,html]
diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc
index 4ee43cc70da034..ba9f6231b18ba8 100644
--- a/docs/src/main/asciidoc/extension-metadata.adoc
+++ b/docs/src/main/asciidoc/extension-metadata.adoc
@@ -102,8 +102,7 @@ metadata:
description: "A Jakarta REST implementation utilizing build time processing and Vert.x.\
\ This extension is not compatible with the quarkus-resteasy extension, or any of\
\ the extensions that depend on it." <4>
-scm:
- url: "https://github.com/quarkusio/quarkus" <5>
+scm-url: "https://github.com/quarkusio/quarkus" <5>
sponsor: A Sponsoring Organisation <6>
----
@@ -111,7 +110,7 @@ sponsor: A Sponsoring Organisation <6>
<2> https://quarkus.io/guides/capabilities[Capabilities] this extension provides
<3> Direct dependencies on other extensions
<4> Description that can be displayed to users. In this case, the description was copied from the `pom.xml` of the extension module but it could also be provided in the template file.
-<5> The source code repository of this extension. Optional, and will often be set automatically. In GitHub Actions builds, it will be inferred from the CI environment. For other GitHub repositories, it can be controlled by setting a `GITHUB_REPOSITORY` environment variable.
+<5> The source code repository of this extension. Optional, and will often be set automatically using the `` information in the pom. In GitHub Actions builds, it will be inferred from the CI environment. For other GitHub repositories, it can be controlled by setting a `GITHUB_REPOSITORY` environment variable.
<6> The sponsor(s) of this extension. Optional, and will sometimes be determined automatically from commit history.
[[quarkus-extension-properties]]
diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc
index 63ef64af6eeb1c..76c6c9c149937b 100644
--- a/docs/src/main/asciidoc/getting-started-testing.adoc
+++ b/docs/src/main/asciidoc/getting-started-testing.adoc
@@ -843,7 +843,20 @@ So if you need to call methods such as `verify` you should hang on to the mock i
==== Further simplification with `@InjectMock`
Building on the features provided by `QuarkusMock`, Quarkus also allows users to effortlessly take advantage of link:https://site.mockito.org/[Mockito] for mocking the beans supported by `QuarkusMock`.
-This functionality is available with the `@io.quarkus.test.InjectMock` annotation if the `quarkus-junit5-mockito` dependency is present.
+
+[IMPORTANT]
+====
+This functionality is available with the `@io.quarkus.test.InjectMock` annotation **only if** the `quarkus-junit5-mockito` dependency is present:
+[source,xml]
+----
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
+----
+
+====
Using `@InjectMock`, the previous example could be written as follows:
@@ -1666,11 +1679,14 @@ The fields annotated with `@Inject` and `@InjectMock` are injected after a test
Finally, the CDI request context is activated and terminated per each test method.
=== Injection
+
Test class fields annotated with `@jakarta.inject.Inject` and `@io.quarkus.test.InjectMock` are injected after a test instance is created.
Dependent beans injected into these fields are correctly destroyed before a test instance is destroyed.
Parameters of a test method for which a matching bean exists are resolved unless annotated with `@io.quarkus.test.component.SkipInject`.
Dependent beans injected into the test method arguments are correctly destroyed after the test method completes.
+NOTE: Arguments of a `@ParameterizedTest` method that are provided by an `ArgumentsProvider`, for example with `@org.junit.jupiter.params.provider.ValueArgumentsProvider`, must be annotated with `@SkipInject`.
+
=== Auto Mocking Unsatisfied Dependencies
Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency.
diff --git a/docs/src/main/asciidoc/grpc-reference.adoc b/docs/src/main/asciidoc/grpc-reference.adoc
new file mode 100644
index 00000000000000..5cfd3e5748873b
--- /dev/null
+++ b/docs/src/main/asciidoc/grpc-reference.adoc
@@ -0,0 +1,232 @@
+////
+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
+////
+= gRPC reference guide
+include::_attributes.adoc[]
+:categories: Serialization
+:diataxis-type: Reference
+:summary: Learn how to configure gRPC server and clients.
+:topics: grpc
+:extensions: io.quarkus:quarkus-grpc
+
+
+== Using gRPC with Quarkus
+
+If you need to implement a gRPC service or consume it, you need the `quarkus-grpc` extension.
+It handles both sides.
+
+=== Using Maven
+
+To enable gRPC, add the following dependency to your project:
+
+[source,xml,subs=attributes+]
+----
+
+ io.quarkus
+ quarkus-grpc
+
+----
+
+Next, ensure that the `generate-code` phase is enabled in the Quarkus Maven plugin:
+
+[source,xml,subs=attributes+]
+----
+
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+----
+
+=== Using Gradle
+
+For Gradle, add the following dependency to your project:
+
+[source,gradle,subs=attributes+]
+----
+implementation 'io.quarkus:quarkus-grpc'
+----
+
+== Selecting a gRPC server
+
+Quarkus provides two implementation of the gRPC server: gRPC Java (based on Netty) and Vert.x.
+Both of them support TLS.
+
+One of the advantage of the Vert.x based server is the ability to use a single server to handle HTTP requests and gRPC requests. This is useful if you want to expose both REST and gRPC endpoints on the same port. This is not possible with the gRPC Java server (using a separate server).
+
+To select the gRPC server implementation, set the `quarkus.grpc.server.use-separate-server` property in your `application.properties` file:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.server.use-separate-server=false # Use the Vert.x based server
+----
+
+We recommend the usage of the Vert.x based gRPC server, as it is more flexible and better integrated in the Quarkus ecosystem.
+
+IMPORTANT: You cannot use both servers at the same time.
+
+== Selecting gRPC clients
+
+As for the server, Quarkus proposes two alternatives for the gRPC clients: gRPC Java and Vert.x.
+Unlike for the server, you can select the transport for each client:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.clients.hello.use-quarkus-grpc-client=true # Use client using the Vert.x based transport
+----
+
+While it's not the default, we recommend using the Vert.x based client, as it is more flexible and better integrated in the Quarkus ecosystem.
+It does not change the stubs you can use, as they are generated by the gRPC framework.
+However, it changes the way the client communicates with the server.
+
+== Configuring TLS for gRPC services
+
+=== With the Vert.x based server
+
+If you use the Vert.x based server, you can configure TLS by setting the following properties in your `application.properties` file:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.server.use-separate-server=false
+
+quarkus.grpc.server.plain-text=false
+quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-tls-keystore.p12
+quarkus.http.ssl.certificate.key-store-password=*****
+quarkus.http.insecure-requests=disabled
+----
+
+You can use `key-store-file` and `key-store-password` to configure the keystore file and its password when using JKS or P12. For PEM, use the `certificate` and `key` properties:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.server.use-separate-server=false
+
+quarkus.grpc.server.plain-text=false
+quarkus.http.ssl.certificate.files=target/certs/grpc-tls.crt
+quarkus.http.ssl.certificate.key-files=target/certs/grpc-tls.key
+quarkus.http.insecure-requests=disabled
+----
+
+NOTE: The `quarkus.http.insecure-requests` property is used to disable insecure requests.
+
+NOTE: When TLS is enabled, it covers both HTTP and gRPC traffic.
+
+=== With the gRPC Java server
+
+If you use the gRPC Java server, you can configure TLS by setting the following properties in your `application.properties` file:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.server.ssl.certificate=tls/server.pem
+quarkus.grpc.server.ssl.key=tls/server.key
+
+quarkus.grpc.server.plain-text=false
+----
+
+This server only supports `PEM` format for the certificate and the key.
+
+== Configuring TLS for gRPC clients
+
+When using the Vert.x based client, you can configure TLS by setting the following properties in your `application.properties` file:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.clients.hello.plain-text=false # Use TLS
+quarkus.grpc.clients.hello.use-quarkus-grpc-client=true # Use client using the Vert.x based transport
+quarkus.grpc.clients.hello.tls.enabled=true
+quarkus.grpc.clients.hello.tls.trust-certificate-p12.path=target/certs/grpc-tls-truststore.jks
+quarkus.grpc.clients.hello.tls.trust-certificate-p12.password=****
+----
+
+If you use JKS trust-store, use the following configuration:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.clients.hello.plain-text=false # Use TLS
+quarkus.grpc.clients.hello.use-quarkus-grpc-client=true # Use client using the Vert.x based transport
+quarkus.grpc.clients.hello.tls.enabled=true
+quarkus.grpc.clients.hello.tls.trust-certificate-jks.path=target/certs/grpc-tls-truststore.jks
+quarkus.grpc.clients.hello.tls.trust-certificate-jks.password=****
+----
+
+If you use PEM certificates as trust-store, use the following configuration:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.clients.hello.plain-text=false # Use TLS
+quarkus.grpc.clients.hello.use-quarkus-grpc-client=true # Use client using the Vert.x based transport
+quarkus.grpc.clients.hello.tls.enabled=true
+quarkus.grpc.clients.hello.tls.trust-certificate-pem.certs=target/certs/grpc-client-ca.crt
+----
+
+When using the gRPC Java client, you can configure TLS by setting the following properties in your `application.properties` file:
+
+[source,properties,subs=attributes+]
+----
+quarkus.grpc.clients.hello.ssl.trust-store=target/certs/grpc-client-tls-ca.crt
+----
+
+gRPC Java client only support the `PEM` format for the trust-store.
+
+== Configuring mTLS
+
+When using the Vert.x based server and Vert.x-based client, you can configure mTLS by setting the following properties in your `application.properties` file:
+
+[source,properties,subs=attributes+]
+----
+# Server side:
+quarkus.grpc.server.use-separate-server=false
+quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+quarkus.http.ssl.certificate.key-store-password=****
+quarkus.http.ssl.certificate.trust-store-file=target/certs/grpc-server-truststore.jks
+quarkus.http.ssl.certificate.trust-store-password=****
+quarkus.http.ssl.client-auth=REQUIRED # Force the client to authenticate, aka mTLS
+quarkus.http.insecure-requests=disabled
+
+# Client side:
+quarkus.grpc.clients.hello.plain-text=false
+quarkus.grpc.clients.hello.tls.trust-certificate-jks.path=target/certs/grpc-client-truststore.jks
+quarkus.grpc.clients.hello.tls.trust-certificate-jks.password=****
+quarkus.grpc.clients.hello.tls.key-certificate-jks.path=target/certs/grpc-client-keystore.jks
+quarkus.grpc.clients.hello.tls.key-certificate-jks.password=****
+quarkus.grpc.clients.hello.tls.enabled=true
+quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+----
+
+If you use P12 format for the trust-store and the key-certificate, use the following configuration:
+
+[source,properties,subs=attributes+]
+----
+# Server side
+quarkus.grpc.server.use-separate-server=false
+quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.p12
+quarkus.http.ssl.certificate.key-store-password=****
+quarkus.http.ssl.certificate.trust-store-file=target/certs/grpc-server-truststore.p12
+quarkus.http.ssl.certificate.trust-store-password=****
+quarkus.http.ssl.client-auth=REQUIRED # Force the client to authenticate, aka mTLS
+quarkus.http.insecure-requests=disabled
+
+# Client side
+quarkus.grpc.clients.hello.plain-text=false
+quarkus.grpc.clients.hello.tls.trust-certificate-p12.path=target/certs/grpc-client-truststore.p12
+quarkus.grpc.clients.hello.tls.trust-certificate-p12.password=****
+quarkus.grpc.clients.hello.tls.key-certificate-p12.path=target/certs/grpc-client-keystore.p12
+quarkus.grpc.clients.hello.tls.key-certificate-p12.password=****
+quarkus.grpc.clients.hello.tls.enabled=true
+quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+----
+
diff --git a/docs/src/main/asciidoc/grpc.adoc b/docs/src/main/asciidoc/grpc.adoc
index 8709b03a2e58d6..d0a7abe6bb1514 100644
--- a/docs/src/main/asciidoc/grpc.adoc
+++ b/docs/src/main/asciidoc/grpc.adoc
@@ -35,3 +35,4 @@ Quarkus gRPC is based on https://vertx.io/docs/vertx-grpc/java/[Vert.x gRPC].
* xref:grpc-kubernetes.adoc[Deploying your gRPC Service in Kubernetes]
* xref:grpc-xds.adoc[Enabling xDS gRPC support]
* xref:grpc-generation-reference.adoc[gRPC code generation reference guide]
+* xref:grpc-reference.adoc[gRPC reference guide]
diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc
index 80422dd69a1a5d..a8fcb8a47eb32f 100644
--- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc
+++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc
@@ -746,22 +746,23 @@ The `Sort` class has plenty of methods for adding columns and specifying sort di
Normally, HQL queries are of this form: `from EntityName [where ...] [order by ...]`, with optional elements
at the end.
-If your select query does not start with `from`, we support the following additional forms:
+If your select query does not start with `from`, `select` or `with`, we support the following additional forms:
- `order by ...` which will expand to `from EntityName order by ...`
-- `` (and single parameter) which will expand to `from EntityName where = ?`
+- `` (and single parameter) which will expand to `from EntityName where = ?`
+- `where ` will expand to `from EntityName where `
- `` will expand to `from EntityName where `
If your update query does not start with `update`, we support the following additional forms:
- `from EntityName ...` which will expand to `update EntityName ...`
-- `set? ` (and single parameter) which will expand to `update EntityName set = ?`
+- `set? ` (and single parameter) which will expand to `update EntityName set = ?`
- `set? ` will expand to `update EntityName set `
If your delete query does not start with `delete`, we support the following additional forms:
- `from EntityName ...` which will expand to `delete from EntityName ...`
-- `` (and single parameter) which will expand to `delete from EntityName where = ?`
+- `` (and single parameter) which will expand to `delete from EntityName where = ?`
- `` will expand to `delete from EntityName where `
NOTE: You can also write your queries in plain
diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc
index e682cc45ceda68..3923a512e4327b 100644
--- a/docs/src/main/asciidoc/hibernate-orm.adoc
+++ b/docs/src/main/asciidoc/hibernate-orm.adoc
@@ -1350,11 +1350,6 @@ and annotating the implementation with the appropriate qualifiers:
@JsonFormat // <1>
@PersistenceUnitExtension // <2>
public class MyJsonFormatMapper implements FormatMapper { // <3>
- @Override
- public String inspect(String sql) {
- // ...
- return sql;
- }
@Override
public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) {
// ...
@@ -1382,11 +1377,6 @@ In case of a custom XML format mapper, a different CDI qualifier must be applied
@XmlFormat // <1>
@PersistenceUnitExtension // <2>
public class MyJsonFormatMapper implements FormatMapper { // <3>
- @Override
- public String inspect(String sql) {
- // ...
- return sql;
- }
@Override
public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) {
// ...
diff --git a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc
index f2decd5f51ec93..5a649ea3ba2f64 100644
--- a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc
+++ b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc
@@ -518,22 +518,23 @@ The `Sort` class has plenty of methods for adding columns and specifying sort di
Normally, HQL queries are of this form: `from EntityName [where ...] [order by ...]`, with optional elements
at the end.
-If your select query does not start with `from`, we support the following additional forms:
+If your select query does not start with `from`, `select` or `with`, we support the following additional forms:
- `order by ...` which will expand to `from EntityName order by ...`
-- `` (and single parameter) which will expand to `from EntityName where = ?`
+- `` (and single parameter) which will expand to `from EntityName where = ?`
+- `where ` will expand to `from EntityName where `
- `` will expand to `from EntityName where `
If your update query does not start with `update`, we support the following additional forms:
-- `from EntityName ...` which will expand to `update from EntityName ...`
-- `set? ` (and single parameter) which will expand to `update from EntityName set = ?`
-- `set? ` will expand to `update from EntityName set `
+- `from EntityName ...` which will expand to `update EntityName ...`
+- `set? ` (and single parameter) which will expand to `update EntityName set = ?`
+- `set? ` will expand to `update EntityName set `
If your delete query does not start with `delete`, we support the following additional forms:
- `from EntityName ...` which will expand to `delete from EntityName ...`
-- `` (and single parameter) which will expand to `delete from EntityName where = ?`
+- `` (and single parameter) which will expand to `delete from EntityName where = ?`
- `` will expand to `delete from EntityName where `
NOTE: You can also write your queries in plain
diff --git a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc
index 9e5d14a1593560..7b36abc3243376 100644
--- a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc
+++ b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc
@@ -759,7 +759,7 @@ as shown below.
[source,properties]
----
-quarkus.hibernate-search-orm.elasticsearch.version=opensearch:1.2
+quarkus.hibernate-search-orm.elasticsearch.version=opensearch:2.11
----
All other configuration options and APIs are exactly the same as with Elasticsearch.
diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc
index 22c9db0150bcc0..dbab826dc68cba 100644
--- a/docs/src/main/asciidoc/http-reference.adoc
+++ b/docs/src/main/asciidoc/http-reference.adoc
@@ -239,8 +239,8 @@ Configure the `quarkus.http.ssl.certificate.reload-period` property to specify t
[source, properties]
----
-quarkus.http.ssl.certificate.files=/mount/certs/cert.pem
-quarkus.http.ssl.certificate.key-files=/mount/certs/key.pem
+quarkus.http.ssl.certificate.files=/mount/certs/tls.crt
+quarkus.http.ssl.certificate.key-files=/mount/certs/tls.key
quarkus.http.ssl.certificate.reload-period=1h
----
diff --git a/docs/src/main/asciidoc/images/auth0-devui-jwt-accesstoken.png b/docs/src/main/asciidoc/images/auth0-devui-jwt-accesstoken.png
new file mode 100644
index 00000000000000..da90d8d5d680e4
Binary files /dev/null and b/docs/src/main/asciidoc/images/auth0-devui-jwt-accesstoken.png differ
diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc
index 120a2977b7e52c..35255f98a82a74 100644
--- a/docs/src/main/asciidoc/kafka.adoc
+++ b/docs/src/main/asciidoc/kafka.adoc
@@ -2048,14 +2048,31 @@ mp.messaging.incoming.data.tracing-enabled=false
If the xref:telemetry-micrometer.adoc[Micrometer extension] is present,
then Kafka producer and consumer clients metrics are exposed as Micrometer meters.
-Per channel metrics are also exposed as Micrometer meters.
-The number of messages produced or received per channel, acknowledgments and duration of processing are exposed.
+=== Channel metrics
-The messaging meters can be disabled:
+Per channel metrics can also be gathered and exposed as Micrometer meters.
+Following metrics can be gathered per channel, identified with the _channel_ tag:
+
+* `quarkus.messaging.message.count` : The number of messages produced or received
+* `quarkus.messaging.message.acks` : The number of messages processed successfully
+* `quarkus.messaging.message.failures` : The number of messages processed with failures
+* `quarkus.messaging.message.duration` : The duration of the message processing.
+
+For backwards compatibility reasons channel metrics are not enabled by default and can be enabled with:
+
+[IMPORTANT]
+====
+The https://smallrye.io/smallrye-reactive-messaging/latest/concepts/observability/[message observation]
+depends on intercepting messages and therefore doesn't support channels consuming messages with
+a custom message type such as `IncomingKafkaRecord`, `KafkaRecord`, `IncomingKafkaRecordBatch` or `KafkaRecordBatch`.
+
+The message interception, and observation, still work with channels consuming the generic `Message` type,
+or custom payloads enabled by https://smallrye.io/smallrye-reactive-messaging/latest/concepts/converters/[converters].
+====
[source, properties]
----
-quarkus.micrometer.binder.messaging.enabled=false
+smallrye.messaging.observation.enabled=true
----
diff --git a/docs/src/main/asciidoc/management-interface-reference.adoc b/docs/src/main/asciidoc/management-interface-reference.adoc
index 345592ca61f9dd..0f245b3875795e 100644
--- a/docs/src/main/asciidoc/management-interface-reference.adoc
+++ b/docs/src/main/asciidoc/management-interface-reference.adoc
@@ -58,6 +58,20 @@ quarkus.management.ssl.certificate.key-store-file=server-keystore.jks
quarkus.management.ssl.certificate.key-store-password=secret
----
+Key store, trust store and certificate files can be reloaded periodically.
+Configure the `quarkus.management.ssl.certificate.reload-period` property to specify the interval at which the certificates should be reloaded:
+
+[source, properties]
+----
+quarkus.http.management.certificate.files=/mount/certs/tls.crt
+quarkus.http.management.certificate.key-files=/mount/certs/tls.key
+quarkus.http.management.certificate.reload-period=1h
+----
+
+The files are reloaded from the same location as they were initially loaded from.
+If there is no content change, the reloading is a no-op.
+It the reloading fails, the server will continue to use the previous certificates.
+
IMPORTANT: Unlike the main HTTP server, the management interface does not handle _http_ and _https_ at the same time.
If _https_ is configured, plain HTTP requests will be rejected.
diff --git a/docs/src/main/asciidoc/picocli.adoc b/docs/src/main/asciidoc/picocli.adoc
index 1d0edcf3ec2413..4ec8182a34c419 100644
--- a/docs/src/main/asciidoc/picocli.adoc
+++ b/docs/src/main/asciidoc/picocli.adoc
@@ -337,10 +337,6 @@ metadata:
name: app
spec:
completionMode: NonIndexed
- selector:
- matchLabels:
- app.kubernetes.io/name: app
- app.kubernetes.io/version: 0.1-SNAPSHOT
suspend: false
template:
metadata:
diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc
index ba56d9283048c2..799ac39c1b0bb2 100644
--- a/docs/src/main/asciidoc/qute-reference.adoc
+++ b/docs/src/main/asciidoc/qute-reference.adoc
@@ -2654,6 +2654,12 @@ The configration value is a regular expression that matches the template path re
For example, `quarkus.qute.dev-mode.no-restart-templates=templates/foo.html` matches the template `src/main/resources/templates/foo.html`.
The matching templates are reloaded and only runtime validations are performed.
+=== Testing
+
+In the test mode, the rendering results of injected and type-safe templates are recorded in the managed `io.quarkus.qute.RenderedResults` which is registered as a CDI bean.
+You can inject `RenderedResults` in a test or any other CDI bean and assert the results.
+However, it's possible to set the `quarkus.qute.test-mode.record-rendered-results` configuration property to `false` to disable this feature.
+
[[type-safe-message-bundles]]
=== Type-safe Message Bundles
diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc
index 12ec1bb96f9102..24709b8fe4c517 100644
--- a/docs/src/main/asciidoc/redis-reference.adoc
+++ b/docs/src/main/asciidoc/redis-reference.adoc
@@ -173,10 +173,16 @@ quarkus.redis.hosts=redis://localhost:5000,redis://localhost:5001,redis://localh
quarkus.redis.client-type=sentinel
# Optional
-quarkus.redis.master-name=my-sentinel # Default is my-master
+quarkus.redis.master-name=my-sentinel # Default is mymaster
quarkus.redis.role=master # master is the default
----
+The host URLs here must be the sentinel servers.
+The client will obtain the URLs of actual Redis servers (master or replicas, depending on `role`) from one of the sentinels, using the `master-name` as an identifier of the "master set".
+
+Note that you practically never want to configure `quarkus.redis.role=sentinel`.
+This setting means that the Redis client will execute commands directly on one of the sentinel servers, instead of an actual Redis server guarded by the sentinels.
+
=== Use the Cluster Mode
When using Redis in cluster mode, you need to pass multiple _host urls_, configure the client type to `cluster` and configure the `replicas` mode:
@@ -188,6 +194,10 @@ quarkus.redis.client-type=cluster
quarkus.redis.replicas=share
----
+The host URLs here must be some of the cluster members.
+Not all cluster members need to be configured, as the client will obtain a full cluster topology from one of the known servers.
+However, it is advisable to configure at least 2 or 3 nodes, not just 1.
+
=== Use the replication Mode
When using the replication mode, you need to pass a single host url and configure the type to be `replication`:
@@ -215,6 +225,8 @@ To use TLS, you need to:
1. Set the `quarkus.redis.tls.enabled=true` property
2. Make sure that your URL starts with `rediss://` (with two `s`)
+IMPORTANT: The default hostname verifier is set to `NONE`, meaning it does not verify the host name. You can change this behavior by setting the `quarkus.redis.tls.hostname-verification-algorithm` property, to `HTTPS` for example.
+
=== Configure the authentication
The Redis password can be set in the `redis://` URL or with the `quarkus.redis.password` property.
diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc
index 21e3b34086a451..7203210e58b95b 100644
--- a/docs/src/main/asciidoc/rest-client-reactive.adoc
+++ b/docs/src/main/asciidoc/rest-client-reactive.adoc
@@ -1681,7 +1681,14 @@ quarkus.rest-client.logging.body-limit=50
quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG
----
-TIP: REST Client Reactive uses a default `ClientLogger` implementation. You can change it by providing a custom `ClientLogger` instance through CDI or when programmatically creating your client.
+[TIP]
+====
+REST Client Reactive uses a default `ClientLogger` implementation, which can be swapped out for a custom implementation.
+
+When setting up the client programmatically using the `QuarkusRestClientBuilder`, the `ClientLogger` is set via the `clientLogger` method.
+
+For declarative clients using `@RegisterRestClient`, simply providing a CDI bean that implements `ClientLogger` is enough for that logger to be used by said clients.
+====
== Mocking the client for tests
If you use a client injected with the `@RestClient` annotation, you can easily mock it for tests.
diff --git a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc
index e46aad7d9fd3a6..09eb2e0a35aa66 100644
--- a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc
+++ b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc
@@ -123,6 +123,10 @@ Quarkus uses smart defaults when determining the media type of Jakarta REST meth
The difference between `quarkus-resteasy-reactive` and `quarkus-resteasy` is the use of `text/plain` as the default media type instead of `text/html`
when the method returns a `String`.
+=== Injection of `@SessionScoped` beans
+
+`@SessionScoped` beans are currently not supported. Should you really need this functionality, you'll need to use RESTEasy Classic instead of RESTEasy Reactive.
+
=== Servlets
RESTEasy Reactive does **not** support servlets.
diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc
index 52278efc7003cf..0670c4e9102e60 100644
--- a/docs/src/main/asciidoc/resteasy-reactive.adoc
+++ b/docs/src/main/asciidoc/resteasy-reactive.adoc
@@ -1005,6 +1005,124 @@ public class Endpoint {
}
----
+=== Concurrent stream element processing
+
+By default, `RestMulti` ensures serial/sequential order of the items/elements produced by the wrapped
+`Multi` by using a value of 1 for the demand signaled to the publishers. To enable concurrent
+processing/generation of multiple items, use `withDemand(long demand)`.
+
+Using a demand higher than 1 is useful when multiple items shall be returned and the production of each
+item takes some time, i.e. when parallel/concurrent production improves the service response time. Be
+aware the concurrent processing also requires more resources and puts a higher load on services or
+resources that are needed to produce the items. Also consider using `Multi.capDemandsTo(long)` and
+`Multi.capDemandsUsing(LongFunction)`.
+
+The example below produces 5 (JSON) strings, but the _order_ of the strings in the returned JSON array
+is not guaranteed. The below example also works for JSON objects and not just simple types.
+
+[source,java]
+----
+package org.acme.rest;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.smallrye.mutiny.Multi;
+import org.jboss.resteasy.reactive.RestMulti;
+
+@Path("message-stream")
+public class Endpoint {
+ @GET
+ public Multi streamMessages() {
+ Multi sourceMulti = Multi
+ .createBy()
+ .merging()
+ .streams(
+ Multi.createFrom().items(
+ "message-1",
+ "message-2",
+ "message-3",
+ "message-4",
+ "message-5"
+ )
+ );
+
+ return RestMulti
+ .fromMultiData(sourceMulti)
+ .withDemand(5)
+ .build();
+ }
+}
+----
+
+Example response, the order is non-deterministic.
+
+[source,text]
+----
+"message-3"
+"message-5"
+"message-4"
+"message-1"
+"message-2"
+----
+
+=== Returning multiple JSON objects
+
+By default, `RestMulti` returns items/elements produced by the wrapped `Multi` as a JSON array, if the
+media-type is `application/json`. To return separate JSON objects that are not wrapped in a JSON array,
+use `encodeAsArray(false)` (`encodeAsArray(true)` is the default). Note that streaming multiple
+objects this way requires a slightly different parsing on the client side, but objects can be parsed and
+consumed as they appear without having to deserialize a possibly huge result at once.
+
+The example below produces 5 (JSON) strings, that are not wrapped in an array, like this:
+
+[source,text]
+----
+"message-1"
+"message-2"
+"message-3"
+"message-4"
+"message-5"
+----
+
+[source,java]
+----
+package org.acme.rest;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.smallrye.mutiny.Multi;
+import org.jboss.resteasy.reactive.RestMulti;
+
+@Path("message-stream")
+public class Endpoint {
+ @GET
+ public Multi streamMessages() {
+ Multi sourceMulti = Multi
+ .createBy()
+ .merging()
+ .streams(
+ Multi.createFrom().items(
+ "message-1",
+ "message-2",
+ "message-3",
+ "message-4",
+ "message-5"
+ )
+ );
+
+ return RestMulti
+ .fromMultiData(sourceMulti)
+ .encodeAsJsonArray(false)
+ .build();
+ }
+}
+----
+
+
=== Server-Sent Event (SSE) support
If you want to stream JSON objects in your response, you can use
diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc
index 25fa988a583150..b2374725ae3a19 100644
--- a/docs/src/main/asciidoc/scheduler-reference.adoc
+++ b/docs/src/main/asciidoc/scheduler-reference.adoc
@@ -34,12 +34,16 @@ Furthermore, the annotated method must return `void` and either declare no param
TIP: The annotation is repeatable so a single method could be scheduled multiple times.
-[WARNING]
-====
-Subclasses never inherit the metadata of a `@Scheduled` method declared on a superclass. In the following example, the `everySecond()` method is only invoked upon the instance of `Jobs`.
+=== Inheritance of metadata
+
+A subclass never inherits the metadata of a `@Scheduled` method declared on a superclass.
+For example, suppose the class `org.amce.Foo` is extended by the class `org.amce.Bar`.
+If `Foo` declares a non-static method annotated with `@Scheduled` then `Bar` does not inherit the metadata of the scheduled method.
+In the following example, the `everySecond()` method is only invoked upon the instance of `Foo`.
+
[source,java]
----
-class Jobs {
+class Foo {
@Scheduled(every = "1s")
void everySecond() {
@@ -48,12 +52,38 @@ class Jobs {
}
@Singleton
-class MyJobs extends Jobs {
+class Bar extends Foo {
}
----
-====
-A CDI event of type `io.quarkus.scheduler.SuccessfulExecution` is fired synchronously and asynchronously when an execution of a scheduled method is successful. A CDI event of type `io.quarkus.scheduler.FailedExecution` is fired synchronously and asynchronously when an execution of a scheduled method throws an exception.
+=== CDI events
+
+Some CDI events are fired synchronously and asynchronously when specific events occur.
+
+|===
+|Type |Event description
+
+|`io.quarkus.scheduler.SuccessfulExecution`
+|An execution of a scheduled job completed successfuly.
+
+|`io.quarkus.scheduler.FailedExecution`
+|An execution of a scheduled job completed with an exception.
+
+|`io.quarkus.scheduler.SkippedExecution`
+|An execution of a scheduled job was skipped.
+
+|`io.quarkus.scheduler.SchedulerPaused`
+|The scheduler was paused.
+
+|`io.quarkus.scheduler.SchedulerResumed`
+|The scheduler was resumed.
+
+|`io.quarkus.scheduler.ScheduledJobPaused`
+|A scheduled job was paused.
+
+|`io.quarkus.scheduler.ScheduledJobResumed`
+|A scheduled job was resumed.
+|===
=== Triggers
@@ -134,7 +164,7 @@ void myMethod() { }
An interval trigger defines a period between invocations.
The period expression is based on the ISO-8601 duration format `PnDTnHnMn.nS` and the value of `@Scheduled#every()` is parsed with `java.time.Duration#parse(CharSequence)`.
-However, if an expression starts with a digit then the `PT` prefix is added automatically.
+However, if an expression starts with a digit and ends with `d`, `P` prefix will be added automatically. If the expression only starts with a digit, `PT` prefix is added automatically.
So for example, `15m` can be used instead of `PT15M` and is parsed as "15 minutes".
.Interval Trigger Example
@@ -207,7 +237,7 @@ NOTE: The final value is always rounded to full second.
`@Scheduled#delayed()` is a text alternative to the properties above.
The period expression is based on the ISO-8601 duration format `PnDTnHnMn.nS` and the value is parsed with `java.time.Duration#parse(CharSequence)`.
-However, if an expression starts with a digit, the `PT` prefix is added automatically.
+However, if an expression starts with a digit and ends with `d`, `P` prefix will be added automatically. If the expression only starts with a digit, `PT` prefix is added automatically.
So for example, `15s` can be used instead of `PT15S` and is parsed as "15 seconds".
[source,java]
diff --git a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc
index 76bfeb30693fcd..af7e03bcbdda88 100644
--- a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc
+++ b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc
@@ -81,6 +81,42 @@ The resulting digest is used as a key for AES-256 encryption of the cookie value
The cookie contains an expiry time as part of the encrypted value, so all nodes in the cluster must have their clocks synchronized.
At one-minute intervals, a new cookie gets generated with an updated expiry time if the session is in use.
+To get started with form authentication, you should have similar settings as described in xref:security-basic-authentication-howto.adoc[Enable Basic authentication] and property `quarkus.http.auth.form.enabled` must be set to `true`.
+
+Simple `application.properties` with form-base authentication can look similar to this:
+[source,properties]
+----
+quarkus.http.auth.form.enabled=true
+
+quarkus.http.auth.form.login-page=login.html
+quarkus.http.auth.form.landing-page=hello
+quarkus.http.auth.form.error-page=
+
+# Define testing user
+quarkus.security.users.embedded.enabled=true
+quarkus.security.users.embedded.plain-text=true
+quarkus.security.users.embedded.users.alice=alice
+quarkus.security.users.embedded.roles.alice=user
+----
+
+[IMPORTANT]
+====
+Configuring user names, secrets, and roles in the application.properties file is appropriate only for testing scenarios. For securing a production application, it is crucial to use a database or LDAP to store this information. For more information you can take a look at xref:security-jpa.adoc[Quarkus Security with Jakarta Persistence] or other mentioned in xref:security-basic-authentication-howto.adoc[Enable Basic authentication].
+====
+
+and application login page will contain HTML form similar to this:
+
+[source,html]
+----
+
+----
+
With single-page applications (SPA), you typically want to avoid redirects by removing default page paths, as shown in the following example:
[source,properties]
diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
index d0e4bb48dc2c5e..723d3aaf279072 100644
--- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
+++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
@@ -65,15 +65,41 @@ quarkus.http.auth.permission.roles1.policy=role-policy1
----
<1> This permission references the default built-in `permit` policy to allow `GET` methods to `/public`.
In this case, the demonstrated setting would not affect this example because this request is allowed anyway.
-<2> This permission references the built-in `deny` policy for `/forbidden`.
+<2> This permission references the built-in `deny` policy for both `/forbidden` and `/forbidden/` paths.
It is an exact path match because it does not end with `*`.
<3> This permission set references the previously defined policy.
`roles1` is an example name; you can call the permission sets whatever you want.
-[WARNING]
+[IMPORTANT]
====
-The exact path `/forbidden` in the example will not secure the `/forbidden/` path.
-It is necessary to add a new exact path for the `/forbidden/` path to ensure proper security coverage.
+The exact path pattern `/forbidden` in the example above also secures the `/forbidden/` path.
+This way, the `forbidden` endpoint in the example below is secured by the `deny1` permission.
+
+[source,java]
+----
+package org.acme.crud;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+@Path("/forbidden")
+public class ForbiddenResource {
+ @GET
+ public String forbidden() { <1>
+ return "No!";
+ }
+}
+----
+<1> Both `/forbidden` and `/forbidden/` paths need to be secured in order to secure the `forbidden` endpoint.
+
+If you need to permit access to the `/forbidden/` path, please add new permission with more specific exact path like in the example below:
+
+[source,properties]
+----
+quarkus.http.auth.permission.permit1.paths=/forbidden/ <1>
+quarkus.http.auth.permission.permit1.policy=permit
+----
+<1> The `/forbidden/` path is not secured.
====
[[custom-http-security-policy]]
diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc
index a60c30a0ca2e08..f8d3de551d00dc 100644
--- a/docs/src/main/asciidoc/security-customization.adoc
+++ b/docs/src/main/asciidoc/security-customization.adoc
@@ -72,6 +72,12 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism
}
----
+TIP: The `HttpAuthenticationMechanism` should transform incoming HTTP request with suitable authentication credentials
+into an `io.quarkus.security.identity.request.AuthenticationRequest` instance and delegate the authentication to the `io.quarkus.security.identity.IdentityProviderManager`.
+Leaving authentication to the `io.quarkus.security.identity.IdentityProvider`s gives you more options for credentials verifications,
+as well as convenient way to perform blocking tasks.
+Nevertheless, the `io.quarkus.security.identity.IdentityProvider` can be omitted and the `HttpAuthenticationMechanism` is free to authenticate request on its own in trivial use cases.
+
[[dealing-with-more-than-one-http-auth-mechanisms]]
== Dealing with more than one HttpAuthenticationMechanism
diff --git a/docs/src/main/asciidoc/security-jdbc.adoc b/docs/src/main/asciidoc/security-jdbc.adoc
index e9447819c373a9..a8718e70ebd108 100644
--- a/docs/src/main/asciidoc/security-jdbc.adoc
+++ b/docs/src/main/asciidoc/security-jdbc.adoc
@@ -167,6 +167,8 @@ quarkus.datasource.jdbc.url=jdbc:postgresql:elytron-security-jdbc
----
In our context, we are using PostgreSQL as identity store, and we initialize the database with users and roles.
+We will use the salted and hashed version of `password` as a password in this example.
+We can use the `BcryptUtil` class to generate passwords in the Modular Crypt Format (MCF).
[source,sql]
----
@@ -177,16 +179,27 @@ CREATE TABLE test_user (
role VARCHAR(255)
);
-INSERT INTO test_user (id, username, password, role) VALUES (1, 'admin', 'admin', 'admin');
-INSERT INTO test_user (id, username, password, role) VALUES (2, 'user','user', 'user');
+INSERT INTO test_user (id, username, password, role) VALUES (1, 'admin', '$2a$10$Uc.SZ0hvGJQlYdsAp7be1.lFjmOnc7aAr4L0YY3/VN3oK.F8zJHRG', 'admin');
+INSERT INTO test_user (id, username, password, role) VALUES (2, 'user','$2a$10$Uc.SZ0hvGJQlYdsAp7be1.lFjmOnc7aAr4L0YY3/VN3oK.F8zJHRG', 'user');
----
-[NOTE]
-====
-It is probably useless, but we kindly remind you that you must not store clear-text passwords in production environment ;-).
-The `elytron-security-jdbc` extension offers a built-in bcrypt password mapper.
-Please refer to the xref:security-getting-started-tutorial.adoc#define-the-user-entity[Define the user entity] section of the Getting started with Security by using Basic authentication and Jakarta Persistence tutorial for practical example.
-====
+When signing up new users, we can encrypt their password as follows:
+
+[source,java]
+----
+package org.acme.security.jdbc;
+
+import io.quarkus.elytron.security.common.BcryptUtil;
+
+public class AccountService {
+
+ public void signupUser(String username, String password) {
+ String encryptedPassword = BcryptUtil.bcryptHash(password);
+
+ // store user with the encrypted password in the database
+ }
+}
+----
We can now configure the Elytron JDBC Realm.
@@ -194,8 +207,10 @@ We can now configure the Elytron JDBC Realm.
----
quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=? <1>
-quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true <2>
-quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true <2>
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1
quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 <3>
quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups
----
@@ -203,7 +218,7 @@ quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups
The `elytron-security-jdbc` extension requires at least one principal query to authenticate the user and its identity.
<1> We define a parameterized SQL statement (with exactly 1 parameter) which should return the user's password plus any additional information you want to load.
-<2> We configure the password mapper with the position of the password field in the `SELECT` fields and other information like salt, hash encoding, etc.
+<2> We configure the password mapper with the position of the password field in the `SELECT` fields and other information like salt, hash encoding, etc. Setting the salt and iteration count indexes to `-1` is required for MCF.
<3> We use `attribute-mappings` to bind the `SELECT` projection fields (i.e. `u.role` here) to the target Principal representation attributes.
[NOTE]
@@ -242,21 +257,21 @@ So far so good, now let's try with an allowed user.
[source,shell]
----
-$ curl -i -X GET -u admin:admin http://localhost:8080/api/admin
+$ curl -i -X GET -u admin:password http://localhost:8080/api/admin
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain;charset=UTF-8
admin%
----
-By providing the `admin:admin` credentials, the extension authenticated the user and loaded their roles.
+By providing the `admin:password` credentials, the extension authenticated the user and loaded their roles.
The `admin` user is authorized to access to the protected resources.
The user `admin` should be forbidden to access a resource protected with `@RolesAllowed("user")` because it doesn't have this role.
[source,shell]
----
-$ curl -i -X GET -u admin:admin http://localhost:8080/api/users/me
+$ curl -i -X GET -u admin:password http://localhost:8080/api/users/me
HTTP/1.1 403 Forbidden
Content-Length: 34
Content-Type: text/html;charset=UTF-8
@@ -268,7 +283,7 @@ Finally, using the user `user` works and the security context contains the princ
[source,shell]
----
-$ curl -i -X GET -u user:user http://localhost:8080/api/users/me
+$ curl -i -X GET -u user:password http://localhost:8080/api/users/me
HTTP/1.1 200 OK
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
@@ -294,8 +309,10 @@ quarkus.datasource.permissions.jdbc.url=jdbc:postgresql:multiple-data-sources-pe
quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=SELECT u.password FROM test_user u WHERE u.username=?
-quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true
-quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1
+quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1
quarkus.security.jdbc.principal-query.roles.sql=SELECT r.role_name FROM test_role r, test_user_role ur WHERE ur.username=? AND ur.role_id = r.id
quarkus.security.jdbc.principal-query.roles.datasource=permissions
diff --git a/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc
index 0983304d2d8396..7c52d1b14c2813 100644
--- a/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc
+++ b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc
@@ -492,9 +492,9 @@ For more information on how to configure Auth0 and Quarkus to have authorization
So far we have only tested the Quarkus endpoint using OIDC authorization code flow. In this flow you use the browser to access the Quarkus endpoint, Quarkus itself manages the authorization code flow, a user is redirected to Auth0, logs in, is redirected back to Quarkus, Quarkus completes the flow by exchanging the code for the ID, access, and refresh tokens, and works with the ID token representing the successful user authentication. The access token is not relevant at the moment. As mentioned earlier, in the authorization code flow, Quarkus will only use the access token to access downstream services on behalf of the currently authenticated user.
-Lets imagine though that the Quarkus endpoint we have developed has to accept `Bearer` access tokens too: it may be that the other Quarkus endpoint which is propagating it to this endpoint or it can be SPA which uses the access token to access the Quarkus endpoint. And Quarkus OIDC DevUI SPA which we already used to analyze the ID token fits perfectly for using the access token available to SPA to test the Quarkus endpoint.
+Let's imagine though that the Quarkus endpoint we have developed has to accept `Bearer` access tokens too: it may be that the other Quarkus endpoint which is propagating it to this endpoint or it can be SPA which uses the access token to access the Quarkus endpoint. And Quarkus OIDC DevUI SPA which we already used to analyze the ID token fits perfectly for using the access token available to SPA to test the Quarkus endpoint.
-Lets go again to http://localhost:8080/q/dev, select the `OpenId Connect` card, login to Auth0, and check the Access token content:
+Let's go again to http://localhost:8080/q/dev-ui, select the `OpenId Connect` card, login to Auth0, and check the Access token content:
image::auth0-devui-accesstoken.png[Auth0 DevUI Access Token]
@@ -632,7 +632,7 @@ For more information about token propagation, see xref:security-openid-connect-c
We have already looked in detail at how Quarkus OIDC can handle <>, but we don't want to propagate Auth0 opaque tokens to micro services which do something useful on behalf on the currently authenticated user, beyond checking its UserInfo.
-A microservice which the front-end Quarkus application will access by propagating authorization code flow access tokens to it is represented in the Auth0 dashboard as an `API`. Lets add it in the `Applications/APIs`:
+A microservice which the front-end Quarkus application will access by propagating authorization code flow access tokens to it is represented in the Auth0 dashboard as an `API`. Let's add it in the `Applications/APIs`:
image::auth0-api.png[Auth0 API]
@@ -804,6 +804,12 @@ public class GreetingResource {
Open a browser, access http://localhost:8080/hello and get your name displayed in the browser.
+Let's go to http://localhost:8080/q/dev-ui, select the `OpenId Connect` card, login to Auth0, and check the Access token content:
+
+image::auth0-devui-jwt-accesstoken.png[Auth0 DevUI JWT Access Token]
+
+As you can see, the access token is no longer encrypted as shown in the <> section and indeed it is in the JWT format now.
+
[[permission-based-access-control]]
=== Permission Based Access Control
@@ -939,7 +945,7 @@ quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
----
-In production, you will distinguish between prod and test level configuration with `%prod.` and `%test.` qualifiers. Lets assume that the above configuration will indeed be prefixed with `%test.` in your real application, with this configuration also including the `%prod.` qualified Auth0 production tenant configuration.
+In production, you will distinguish between prod and test level configuration with `%prod.` and `%test.` qualifiers. Let's assume that the above configuration will indeed be prefixed with `%test.` in your real application, with this configuration also including the `%prod.` qualified Auth0 production tenant configuration.
Using `OidcTestClient` to test such configuration requires acquiring a token from the Auth0 dev tenant, using either OAuth2 `password` or `client_credentials` grant, we will try a `password` grant. Make sure the application registered in the Auth0 dashboard allows the `password` grant:
diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
index a4a346d4e7fdc8..9f0e7a32431c5c 100644
--- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
+++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
@@ -298,7 +298,7 @@ quarkus.oidc.introspection-credentials.secret=introspection-user-secret
[[oidc-request-filters]]
==== OIDC request filters
-You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations, which can update or add new request headers and can also log requests.
+You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFilter` implementations, which can update or add new request headers and can also log requests.
For example:
diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc
index 3938d8a629d4e7..155aa0516065f8 100644
--- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc
+++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc
@@ -702,6 +702,71 @@ quarkus.oidc-client.credentials.jwt.subject=custom-subject
quarkus.oidc-client.credentials.jwt.issuer=custom-issuer
----
+==== JWT Bearer
+
+link:https://www.rfc-editor.org/rfc/rfc7523[RFC7523] explains how JWT Bearer tokens can be used to authenticate clients, see the link:https://www.rfc-editor.org/rfc/rfc7523#section-2.2[Using JWTs for Client Authentication] section for more information.
+
+It can be enabled as follows:
+
+[source,properties]
+----
+quarkus.oidc-client.auth-server-url=${auth-server-url}
+quarkus.oidc-client.client-id=quarkus-app
+quarkus.oidc-client.credentials.jwt.source=bearer
+----
+
+Next, the JWT bearer token must be provided as a `client_assertion` parameter to the OIDC client.
+
+You can use `OidcClient` methods for acquiring or refreshing tokens which accept additional grant parameters, for example, `oidcClient.getTokens(Map.of("client_assertion", "ey..."))`.
+
+If you work work with the OIDC client filters then you must register a custom filter which will provide this assertion.
+
+Here is an example of the RestEasy Reactive custom filter:
+
+[source,java]
+----
+package io.quarkus.it.keycloak;
+
+import java.util.Map;
+
+import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter;
+import io.quarkus.oidc.common.runtime.OidcConstants;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.Priorities;
+
+@Priority(Priorities.AUTHENTICATION)
+public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestReactiveFilter {
+
+ @Override
+ protected Map additionalParameters() {
+ return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
+ }
+}
+----
+
+Here is an example of the RestEasy Classic custom filter:
+
+[source,java]
+----
+package io.quarkus.it.keycloak;
+
+import java.util.Map;
+
+import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter;
+import io.quarkus.oidc.common.runtime.OidcConstants;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.Priorities;
+
+@Priority(Priorities.AUTHENTICATION)
+public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestFilter {
+
+ @Override
+ protected Map additionalParameters() {
+ return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
+ }
+}
+----
+
==== Apple POST JWT
Apple OpenID Connect Provider uses a `client_secret_post` method where a secret is a JWT produced with a `private_key_jwt` authentication method but with Apple account-specific issuer and subject properties.
@@ -879,7 +944,7 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-lev
[[oidc-request-filters]]
== OIDC request filters
-You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations, which can update or add new request headers. For example, a filter can analyze the request body and add its digest as a new header value:
+You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFilter` implementations, which can update or add new request headers. For example, a filter can analyze the request body and add its digest as a new header value:
[source,java]
----
@@ -966,8 +1031,9 @@ quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
-quarkus.oidc-token-propagation.exchange-token=true
+quarkus.oidc-token-propagation.exchange-token=true <1>
----
+<1> Please note that the `exchange-token` configuration property is ignored when the OidcClient name is set with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute.
Note `AccessTokenRequestReactiveFilter` will use `OidcClient` to exchange the current token, and you can use `quarkus.oidc-client.grant-options.exchange` to set the additional exchange properties expected by your OpenID Connect Provider.
@@ -986,7 +1052,7 @@ quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.oidc-token-propagation-reactive.exchange-token=true
----
-`AccessTokenRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation-reactive.client-name` configuration property.
+`AccessTokenRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation-reactive.client-name` configuration property or with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute.
[[token-propagation]]
== Token Propagation
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 5645acfd4a8c7b..36a3fe40541c2e 100644
--- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc
+++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc
@@ -258,7 +258,7 @@ For more information, see xref:security-oidc-bearer-token-authentication.adoc#in
[[keycloak-initialization]]
=== Keycloak initialization
-The `quay.io/keycloak/keycloak:23.0.4` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default.
+The `quay.io/keycloak/keycloak:23.0.7` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default.
`quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name.
For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly.
Be aware that a Quarkus-based Keycloak distribution is only available starting from Keycloak `20.0.0`.
diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc
index 546e4c0e17c564..b98845ca537e85 100644
--- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc
+++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc
@@ -724,6 +724,19 @@ public class HelloResource {
----
<1> The `io.quarkus.oidc.Tenant` annotation must be placed on either the resource class or resource method.
+[[TIP]]
+In the example above, authentication of the `sayHello` endpoint is enforced with the `@Authenticated` annotation.
+Alternatively, if you use an the xref:security-authorize-web-endpoints-reference.adoc#authorization-using-configuration[HTTP Security policy]
+to secure the endpoint, then, for the `@Tenant` annotation be effective, you must delay this policy's permission check as shown in the example below:
+[source,properties]
+----
+quarkus.http.auth.permission.authenticated.paths=/api/hello
+quarkus.http.auth.permission.authenticated.methods=GET
+quarkus.http.auth.permission.authenticated.policy=authenticated
+quarkus.http.auth.permission.authenticated.applies-to=JAXRS <1>
+----
+<1> Tell Quarkus to run the HTTP permission check after the tenant has been selected with the `@Tenant` annotation.
+
[[tenant-config-resolver]]
== Dynamic tenant configuration resolution
diff --git a/docs/src/main/asciidoc/vertx-reference.adoc b/docs/src/main/asciidoc/vertx-reference.adoc
index b7956de6f443be..0d940abd968b88 100644
--- a/docs/src/main/asciidoc/vertx-reference.adoc
+++ b/docs/src/main/asciidoc/vertx-reference.adoc
@@ -27,13 +27,14 @@ With this extension, you can retrieve the managed instance of Vert.x using eithe
----
@ApplicationScoped
public class MyBean {
-// Field injection
-@Inject Vertx vertx;
-// Constructor injection
-MyBean(Vertx vertx) {
- // ...
-}
+ // Field injection
+ @Inject Vertx vertx;
+
+ // Constructor injection
+ MyBean(Vertx vertx) {
+ // ...
+ }
}
----
@@ -83,7 +84,7 @@ Check the associated documentation to learn how to use them.
|AMQP Client
|`io.quarkus:quarkus-smallrye-reactive-messaging-amqp` (extension)
-|xref:amqp.adoc
+|xref:amqp.adoc[Getting Started to SmallRye Reactive Messaging with AMQP]
|Circuit Breaker
|`io.smallrye.reactive:smallrye-mutiny-vertx-circuit-breaker` (external dependency)
@@ -95,15 +96,15 @@ Check the associated documentation to learn how to use them.
|DB2 Client
|`io.quarkus:quarkus-reactive-db2-client` (extension)
-|xref:reactive-sql-clients.adoc
+|xref:reactive-sql-clients.adoc[Reactive SQL Clients]
|Kafka Client
|`io.quarkus:quarkus-smallrye-reactive-messaging-kafka` (extension)
-|xref:kafka.adoc
+|xref:kafka.adoc[Apache Kafka Reference Guide]
|Mail Client
|`io.quarkus:quarkus-mailer` (extension)
-|xref:mailer.adoc
+|xref:mailer.adoc[Sending emails using SMTP]
|MQTT Client
|`io.quarkus:quarkus-smallrye-reactive-messaging-mqtt` (extension)
@@ -111,19 +112,19 @@ Check the associated documentation to learn how to use them.
|MS SQL Client
|`io.quarkus:quarkus-reactive-mssql-client` (extension)
-|xref:reactive-sql-clients.adoc
+|xref:reactive-sql-clients.adoc[Reactive SQL Clients]
|MySQL Client
|`io.quarkus:quarkus-reactive-mysql-client` (extension)
-|xref:reactive-sql-clients.adoc
+|xref:reactive-sql-clients.adoc[Reactive SQL Clients]
|Oracle Client
|`io.quarkus:quarkus-reactive-oracle-client` (extension)
-|xref:reactive-sql-clients.adoc
+|xref:reactive-sql-clients.adoc[Reactive SQL Clients]
|PostgreSQL Client
|`io.quarkus:quarkus-reactive-pg-client` (extension)
-|xref:reactive-sql-clients.adoc
+|xref:reactive-sql-clients.adoc[Reactive SQL Clients]
|RabbitMQ Client
|`io.smallrye.reactive:smallrye-mutiny-vertx-rabbitmq-client` (external dependency)
@@ -131,7 +132,7 @@ Check the associated documentation to learn how to use them.
|Redis Client
|`io.quarkus:quarkus-redis-client` (extension)
-|xref:redis.adoc
+|xref:redis.adoc[Using the Redis Client]
|Web Client
|`io.smallrye.reactive:smallrye-mutiny-vertx-web-client` (external dependency)
diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc
index 36f38ed605e3eb..9ce12cb7c48e7a 100644
--- a/docs/src/main/asciidoc/writing-extensions.adoc
+++ b/docs/src/main/asciidoc/writing-extensions.adoc
@@ -384,8 +384,12 @@ Your extension project should be setup as a multi-module project with two submod
Your runtime artifact should depend on `io.quarkus:quarkus-core`, and possibly the runtime artifacts of other Quarkus
modules if you want to use functionality provided by them.
+
Your deployment time module should depend on `io.quarkus:quarkus-core-deployment`, your runtime artifact,
-and possibly the deployment artifacts of other Quarkus modules if you want to use functionality provided by them.
+and the deployment artifacts of any other Quarkus extensions your own extension depends on. This is essential, otherwise any transitively
+pulled in extensions will not provide their full functionality.
+
+NOTE: The Maven and Gradle plugins will validate this for you and alert you to any deployment artifacts you might have forgotten to add.
[WARNING]
====
@@ -573,10 +577,6 @@ dependencies {
}
----
-[WARNING]
-====
-This plugin is still experimental, it does not validate the extension dependencies as the equivalent Maven plugin does.
-====
[[build-step-processors]]
=== Build Step Processors
diff --git a/docs/src/main/asciidoc/writing-native-applications-tips.adoc b/docs/src/main/asciidoc/writing-native-applications-tips.adoc
index 10e9e43b4a6fcd..2dd53e2c76bd74 100644
--- a/docs/src/main/asciidoc/writing-native-applications-tips.adoc
+++ b/docs/src/main/asciidoc/writing-native-applications-tips.adoc
@@ -60,7 +60,9 @@ will include:
==== Using a configuration file
If globs are not sufficiently precise for your use case and you need to rely on regular expressions, or if you prefer relying on the GraalVM infrastructure,
-you can also create a `resource-config.json` (the most common location is within `src/main/resources`) JSON file defining which resources should be included.
+you can also create a `resource-config.json` JSON file defining which resources should be included.
+Ideally this, and other native image configuration files, should be placed under the `src/main/resources/META-INF/native-image//` folder.
+This way they will be automatically parsed by the native build, without additional configuration.
[WARNING]
====
@@ -93,60 +95,6 @@ Here we include all the XML files and JSON files into the native executable.
For more information about this topic, see the link:https://www.graalvm.org/{graalvm-docs-version}/reference-manual/native-image/dynamic-features/Resources/[GraalVM Accessing Resources in Native Image] guide.
====
-The final order of business is to make the configuration file known to the `native-image` executable by adding the proper configuration to `application.properties`:
-
-[source,properties]
-----
-quarkus.native.additional-build-args =\
- -H:+UnlockExperimentalVMOptions,\
- -H:ResourceConfigurationFiles=resource-config.json,\
- -H:-UnlockExperimentalVMOptions
-----
-
-[NOTE]
-====
-Starting with Mandrel 23.1 and GraalVM for JDK 21, `-H:ResourceConfigurationFiles=resource-config.json` results in a warning being shown unless wrapped in `-H:+UnlockExperimentalVMOptions` and `-H:-UnlockExperimentalVMOptions`.
-The absence of these options will result in build failures in the future.
-====
-
-In the previous snippet we were able to simply use `resource-config.json` instead of specifying the entire path of the file simply because it was added to `src/main/resources`.
-If the file had been added to another directory, the proper file path would have had to be specified manually.
-
-[TIP]
-====
-Multiple options may be separated by a comma. For example, one could use:
-
-[source,properties]
-----
-quarkus.native.additional-build-args =\
- -H:+UnlockExperimentalVMOptions,\
- -H:ResourceConfigurationFiles=resource-config.json,\
- -H:ReflectionConfigurationFiles=reflect-config.json,\
- -H:-UnlockExperimentalVMOptions
-----
-
-in order to ensure that various resources are included and additional reflection is registered.
-
-====
-If for some reason adding the aforementioned configuration to `application.properties` is not desirable, it is possible to configure the build tool to effectively perform the same operation.
-
-When using Maven, we could use the following configuration:
-
-[source,xml]
-----
-
-
- native
-
- native
-
- -H:+UnlockExperimentalVMOptions,-H:ResourceConfigurationFiles=resource-config.json,-H:-UnlockExperimentalVMOptions
-
-
-
-
-----
-
=== Registering for reflection
When building a native executable, GraalVM operates with a closed world assumption.
@@ -280,59 +228,9 @@ As an example, in order to register all methods of class `com.acme.MyClass` for
For more information about the format of this file, see the link:https://www.graalvm.org/{graalvm-docs-version}/reference-manual/native-image/dynamic-features/Reflection/[GraalVM Reflection in Native Image] guide.
====
-The final order of business is to make the configuration file known to the `native-image` executable by adding the proper configuration to `application.properties`:
-
-[source,properties]
-----
-quarkus.native.additional-build-args =\
- -H:+UnlockExperimentalVMOptions,\
- -H:ReflectionConfigurationFiles=reflect-config.json,\
- -H:-UnlockExperimentalVMOptions
-----
-
-[NOTE]
-====
-Starting with Mandrel 23.1 and GraalVM for JDK 21, `-H:ResourceConfigurationFiles=resource-config.json` results in a warning being shown unless wrapped in `-H:+UnlockExperimentalVMOptions` and `-H:-UnlockExperimentalVMOptions`.
-The absence of these options will result in build failures in the future.
-====
-
-In the previous snippet we were able to simply use `reflect-config.json` instead of specifying the entire path of the file simply because it was added to `src/main/resources`.
-If the file had been added to another directory, the proper file path would have had to be specified manually.
-
-[TIP]
-====
-Multiple options may be separated by a comma. For example, one could use:
-
-[source,properties]
-----
-quarkus.native.additional-build-args =\
- -H:+UnlockExperimentalVMOptions,\
- -H:ResourceConfigurationFiles=resource-config.json,\
- -H:ReflectionConfigurationFiles=reflect-config.json,\
- -H:-UnlockExperimentalVMOptions
-----
-
-in order to ensure that various resources are included and additional reflection is registered.
-
-====
-If for some reason adding the aforementioned configuration to `application.properties` is not desirable, it is possible to configure the build tool to effectively perform the same operation.
-
-When using Maven, we could use the following configuration:
-
-[source,xml]
-----
-
-
- native
-
- native
-
- -H:+UnlockExperimentalVMOptions,-H:ReflectionConfigurationFiles=reflect-config.json,-H:-UnlockExperimentalVMOptions
-
-
-
-
-----
+The final order of business is to make the configuration file known to the `native-image` executable.
+To do that, place the configuration file under the `src/main/resources/META-INF/native-image//` folder.
+This way they will be automatically parsed by the native build, without additional configuration.
[[delay-class-init-in-your-app]]
=== Delaying class initialization
diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java
index 3fc83fad2d6754..3b8694dcc50252 100644
--- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java
+++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java
@@ -180,7 +180,7 @@ public AgroalDataSource doCreateDataSource(String dataSourceName, boolean failIf
}
DataSourceJdbcRuntimeConfig dataSourceJdbcRuntimeConfig = dataSourcesJdbcRuntimeConfig
- .getDataSourceJdbcRuntimeConfig(dataSourceName);
+ .dataSources().get(dataSourceName).jdbc();
if (!dataSourceJdbcRuntimeConfig.url().isPresent()) {
//this is not an error situation, because we want to allow the situation where a JDBC extension
//is installed but has not been configured
diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcBuildTimeConfig.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcBuildTimeConfig.java
index ed47c6fbff8885..b8a76bf2158d42 100644
--- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcBuildTimeConfig.java
+++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcBuildTimeConfig.java
@@ -4,7 +4,6 @@
import io.quarkus.datasource.common.runtime.DataSourceUtil;
import io.quarkus.runtime.annotations.ConfigDocMapKey;
-import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
@@ -20,7 +19,6 @@ public interface DataSourcesJdbcBuildTimeConfig {
/**
* Datasources.
*/
- @ConfigDocSection
@ConfigDocMapKey("datasource-name")
@WithParentName
@WithDefaults
diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcRuntimeConfig.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcRuntimeConfig.java
index a2b406a2dc6ecb..359a7ccdcebeeb 100644
--- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcRuntimeConfig.java
+++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourcesJdbcRuntimeConfig.java
@@ -4,34 +4,29 @@
import io.quarkus.datasource.common.runtime.DataSourceUtil;
import io.quarkus.runtime.annotations.ConfigDocMapKey;
-import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefaults;
import io.smallrye.config.WithParentName;
+import io.smallrye.config.WithUnnamedKey;
@ConfigMapping(prefix = "quarkus.datasource")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public interface DataSourcesJdbcRuntimeConfig {
/**
- * The default datasource.
+ * Datasources.
*/
- DataSourceJdbcRuntimeConfig jdbc();
-
- /**
- * Additional named datasources.
- */
- @ConfigDocSection
@ConfigDocMapKey("datasource-name")
@WithParentName
@WithDefaults
- Map namedDataSources();
+ @WithUnnamedKey(DataSourceUtil.DEFAULT_DATASOURCE_NAME)
+ Map dataSources();
@ConfigGroup
- public interface DataSourceJdbcOuterNamedRuntimeConfig {
+ interface DataSourceJdbcOuterNamedRuntimeConfig {
/**
* The JDBC runtime configuration.
@@ -39,11 +34,4 @@ public interface DataSourceJdbcOuterNamedRuntimeConfig {
DataSourceJdbcRuntimeConfig jdbc();
}
- default DataSourceJdbcRuntimeConfig getDataSourceJdbcRuntimeConfig(String dataSourceName) {
- if (DataSourceUtil.isDefault(dataSourceName)) {
- return jdbc();
- }
-
- return namedDataSources().get(dataSourceName).jdbc();
- }
}
diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java
index 7a6d03fee425a1..4a6e3db63be68c 100644
--- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java
+++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java
@@ -18,6 +18,7 @@
import org.eclipse.microprofile.health.Readiness;
import io.agroal.api.AgroalDataSource;
+import io.quarkus.agroal.runtime.AgroalDataSourceSupport;
import io.quarkus.agroal.runtime.DataSources;
import io.quarkus.arc.Arc;
import io.quarkus.datasource.common.runtime.DataSourceUtil;
@@ -40,9 +41,10 @@ protected void init() {
}
DataSourceSupport support = Arc.container().instance(DataSourceSupport.class)
.get();
- Set names = support.getConfiguredNames();
+ AgroalDataSourceSupport agroalSupport = Arc.container().instance(AgroalDataSourceSupport.class)
+ .get();
Set excludedNames = support.getInactiveOrHealthCheckExcludedNames();
- for (String name : names) {
+ for (String name : agroalSupport.entries.keySet()) {
if (excludedNames.contains(name)) {
continue;
}
diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/synthetic/SyntheticBeanBuildItemProxyTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/synthetic/SyntheticBeanBuildItemProxyTest.java
index 50275150a21626..6ce4b6c9d54771 100644
--- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/synthetic/SyntheticBeanBuildItemProxyTest.java
+++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/synthetic/SyntheticBeanBuildItemProxyTest.java
@@ -1,10 +1,11 @@
package io.quarkus.arc.test.synthetic;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Method;
+import java.util.List;
import java.util.function.Consumer;
import jakarta.enterprise.context.ApplicationScoped;
@@ -14,6 +15,7 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import io.quarkus.arc.Arc;
+import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator;
import io.quarkus.builder.BuildChainBuilder;
@@ -46,20 +48,31 @@ public void execute(BuildContext context) {
// We need to use reflection due to some class loading problems
Object recorderProxy = bytecodeRecorder.getRecordingProxy(TestRecorder.class);
try {
- Method test = recorderProxy.getClass().getDeclaredMethod("test");
- Object proxy = test.invoke(recorderProxy);
- ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem.configure(SynthBean.class)
+ Method test = recorderProxy.getClass().getDeclaredMethod("test", String.class);
+
+ Object proxy1 = test.invoke(recorderProxy, "ok");
+ ExtendedBeanConfigurator configurator1 = SyntheticBeanBuildItem.configure(SynthBean.class)
.scope(ApplicationScoped.class)
+ .identifier("ok")
.unremovable();
// No creator
assertThrows(IllegalStateException.class,
- () -> configurator.done());
+ () -> configurator1.done());
// Not a returned proxy
assertThrows(IllegalArgumentException.class,
- () -> configurator.runtimeProxy(new SynthBean()));
- context.produce(configurator
- .runtimeProxy(proxy)
+ () -> configurator1.runtimeProxy(new SynthBean()));
+ context.produce(configurator1
+ .runtimeProxy(proxy1)
+ .done());
+
+ // Register a synthetic bean with same types and qualifiers but different identifier
+ context.produce(SyntheticBeanBuildItem.configure(SynthBean.class)
+ .scope(ApplicationScoped.class)
+ .identifier("nok")
+ .unremovable()
+ .runtimeProxy(test.invoke(recorderProxy, "nok"))
.done());
+
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -73,9 +86,9 @@ public void execute(BuildContext context) {
@Recorder
public static class TestRecorder {
- public SynthBean test() {
+ public SynthBean test(String val) {
SynthBean bean = new SynthBean();
- bean.setValue("ok");
+ bean.setValue(val);
return bean;
}
@@ -83,9 +96,12 @@ public SynthBean test() {
@Test
public void testBeans() {
- SynthBean bean = Arc.container().instance(SynthBean.class).get();
- assertNotNull(bean);
- assertEquals("ok", bean.getValue());
+ List> beans = Arc.container().listAll(SynthBean.class);
+ assertEquals(2, beans.size());
+ for (InstanceHandle handle : beans) {
+ String val = handle.get().getValue();
+ assertTrue("ok".equals(val) || "nok".equals(val));
+ }
}
@Vetoed
diff --git a/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/Function.java b/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/Function.java
index dde4d70e945c0e..3bfb8bdd2684be 100644
--- a/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/Function.java
+++ b/extensions/azure-functions-http/runtime/src/main/java/io/quarkus/azure/functions/resteasy/runtime/Function.java
@@ -3,7 +3,6 @@
import java.util.Optional;
import com.microsoft.azure.functions.ExecutionContext;
-import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
@@ -16,10 +15,8 @@ public class Function extends BaseFunction {
@FunctionName(QUARKUS_HTTP)
public HttpResponseMessage run(
- @HttpTrigger(name = "req", dataType = "binary", methods = { HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST,
- HttpMethod.PUT,
- HttpMethod.OPTIONS }, route = "{*path}", authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request,
- final ExecutionContext context) {
+ @HttpTrigger(name = "req", dataType = "binary", route = "{*path}", authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request,
+ ExecutionContext context) {
return dispatch(request);
}
diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
index 843f5a04802382..90c49d23397805 100644
--- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
+++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
@@ -101,6 +101,8 @@ public class JibProcessor {
private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.18", UBI8_PREFIX, OPENJDK_PREFIX,
RUNTIME_SUFFIX);
+ // The source for this can be found at https://github.com/jboss-container-images/openjdk/blob/ubi8/modules/run/artifacts/opt/jboss/container/java/run/run-java.sh
+ // A list of env vars that affect this script can be found at https://jboss-container-images.github.io/openjdk/ubi8/ubi8-openjdk-17.html
private static final String RUN_JAVA_PATH = "/opt/jboss/container/java/run/run-java.sh";
private static final String DEFAULT_BASE_IMAGE_USER = "185";
@@ -455,6 +457,7 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag
// which would mean AppCDS would not be taken into account at all
entrypoint = List.of(RUN_JAVA_PATH);
envVars.put("JAVA_APP_JAR", workDirInContainer + "/" + JarResultBuildStep.QUARKUS_RUN_JAR);
+ envVars.put("JAVA_APP_DIR", workDirInContainer.toString());
envVars.put("JAVA_OPTS_APPEND", String.join(" ", determineEffectiveJvmArguments(jibConfig, appCDSResult)));
} else {
List effectiveJvmArguments = determineEffectiveJvmArguments(jibConfig, appCDSResult);
@@ -526,13 +529,14 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag
try {
Instant now = Instant.now();
+ boolean enforceModificationTime = !jibConfig.useCurrentTimestampFileModification;
Instant modificationTime = jibConfig.useCurrentTimestampFileModification ? now : Instant.EPOCH;
JibContainerBuilder jibContainerBuilder = toJibContainerBuilder(baseJvmImage, jibConfig);
if (fastChangingLibPaths.isEmpty()) {
// just create a layer with the entire lib structure intact
addLayer(jibContainerBuilder, Collections.singletonList(componentsPath.resolve(JarResultBuildStep.LIB)),
- workDirInContainer, "fast-jar-lib", isMutableJar, modificationTime);
+ workDirInContainer, "fast-jar-lib", isMutableJar, enforceModificationTime, modificationTime);
} else {
// we need to manually create each layer
// the idea here is that the fast changing libraries are created in a later layer, thus when they do change,
@@ -546,14 +550,9 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag
AbsoluteUnixPath libPathInContainer = workDirInContainer.resolve(JarResultBuildStep.LIB)
.resolve(JarResultBuildStep.BOOT_LIB)
.resolve(lib.getFileName());
- if (appCDSResult.isPresent()) {
- // the boot lib jars need to preserve the modification time because otherwise AppCDS won't work
- bootLibsLayerBuilder.addEntry(lib, libPathInContainer,
- Files.getLastModifiedTime(lib).toInstant());
- } else {
- bootLibsLayerBuilder.addEntry(lib, libPathInContainer);
- }
-
+ // the boot lib jars need to preserve the modification time because otherwise AppCDS won't work
+ bootLibsLayerBuilder.addEntry(lib, libPathInContainer,
+ Files.getLastModifiedTime(lib).toInstant());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -566,15 +565,15 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag
.resolve(JarResultBuildStep.DEPLOYMENT_LIB);
addLayer(jibContainerBuilder, Collections.singletonList(deploymentPath),
workDirInContainer.resolve(JarResultBuildStep.LIB),
- "fast-jar-deployment-libs", true, modificationTime);
+ "fast-jar-deployment-libs", true, enforceModificationTime, modificationTime);
}
AbsoluteUnixPath libsMainPath = workDirInContainer.resolve(JarResultBuildStep.LIB)
.resolve(JarResultBuildStep.MAIN);
addLayer(jibContainerBuilder, nonFastChangingLibPaths, libsMainPath, "fast-jar-normal-libs",
- isMutableJar, modificationTime);
+ isMutableJar, enforceModificationTime, modificationTime);
addLayer(jibContainerBuilder, new ArrayList<>(fastChangingLibPaths), libsMainPath, "fast-jar-changing-libs",
- isMutableJar, modificationTime);
+ isMutableJar, enforceModificationTime, modificationTime);
}
if (appCDSResult.isPresent()) {
@@ -598,9 +597,9 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag
}
addLayer(jibContainerBuilder, Collections.singletonList(componentsPath.resolve(JarResultBuildStep.APP)),
- workDirInContainer, "fast-jar-quarkus-app", isMutableJar, modificationTime);
+ workDirInContainer, "fast-jar-quarkus-app", isMutableJar, enforceModificationTime, modificationTime);
addLayer(jibContainerBuilder, Collections.singletonList(componentsPath.resolve(JarResultBuildStep.QUARKUS)),
- workDirInContainer, "fast-jar-quarkus", isMutableJar, modificationTime);
+ workDirInContainer, "fast-jar-quarkus", isMutableJar, enforceModificationTime, modificationTime);
if (ContainerImageJibConfig.DEFAULT_WORKING_DIR.equals(jibConfig.workingDirectory)) {
// this layer ensures that the working directory is writeable
// see https://github.com/GoogleContainerTools/jib/issues/1270
@@ -664,7 +663,7 @@ private boolean containsRunJava(String baseJvmImage) {
public JibContainerBuilder addLayer(JibContainerBuilder jibContainerBuilder, List files,
AbsoluteUnixPath pathInContainer, String name, boolean isMutableJar,
- Instant now)
+ boolean enforceModificationTime, Instant forcedModificationTime)
throws IOException {
FileEntriesLayer.Builder layerConfigurationBuilder = FileEntriesLayer.builder().setName(name);
@@ -672,7 +671,17 @@ public JibContainerBuilder addLayer(JibContainerBuilder jibContainerBuilder, Lis
layerConfigurationBuilder.addEntryRecursive(
file, pathInContainer.resolve(file.getFileName()),
isMutableJar ? REMOTE_DEV_FOLDER_PERMISSIONS_PROVIDER : DEFAULT_FILE_PERMISSIONS_PROVIDER,
- (sourcePath, destinationPath) -> now,
+ (sourcePath, destinationPath) -> {
+ if (enforceModificationTime) {
+ return forcedModificationTime;
+ }
+
+ try {
+ return Files.getLastModifiedTime(sourcePath).toInstant();
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to get last modified time for " + sourcePath, e);
+ }
+ },
isMutableJar ? REMOTE_DEV_OWNERSHIP_PROVIDER : DEFAULT_OWNERSHIP_PROVIDER);
}
diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java
index 87a39c554f6c3d..3f4b459505df80 100644
--- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java
+++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java
@@ -15,20 +15,12 @@ public class DataSourceRecorder {
public RuntimeValue createDataSourceSupport(
DataSourcesBuildTimeConfig buildTimeConfig,
DataSourcesRuntimeConfig runtimeConfig) {
- Stream.Builder configured = Stream.builder();
Stream.Builder excludedForHealthChecks = Stream.builder();
for (Map.Entry dataSource : buildTimeConfig.dataSources().entrySet()) {
- // TODO this is wrong, as the default datasource could be configured without db-kind being set:
- // it's inferred automatically for the default datasource when possible.
- // See https://github.com/quarkusio/quarkus/issues/37779
- if (dataSource.getValue().dbKind().isPresent()) {
- configured.add(dataSource.getKey());
- }
if (dataSource.getValue().healthExclude()) {
excludedForHealthChecks.add(dataSource.getKey());
}
}
- Set names = configured.build().collect(toUnmodifiableSet());
Set excludedNames = excludedForHealthChecks.build().collect(toUnmodifiableSet());
Stream.Builder inactive = Stream.builder();
@@ -39,6 +31,6 @@ public RuntimeValue createDataSourceSupport(
}
Set inactiveNames = inactive.build().collect(toUnmodifiableSet());
- return new RuntimeValue<>(new DataSourceSupport(names, excludedNames, inactiveNames));
+ return new RuntimeValue<>(new DataSourceSupport(excludedNames, inactiveNames));
}
}
diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java
index 96b4b0f1fa9a9d..21ea9141a98cea 100644
--- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java
+++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java
@@ -12,26 +12,17 @@
*/
public class DataSourceSupport {
- private final Set configuredNames;
private final Set inactiveNames;
private final Set inactiveOrHealthCheckExcludedNames;
- public DataSourceSupport(Set configuredNames, Set healthCheckExcludedNames,
+ public DataSourceSupport(Set healthCheckExcludedNames,
Set inactiveNames) {
- this.configuredNames = configuredNames;
this.inactiveOrHealthCheckExcludedNames = new HashSet<>();
inactiveOrHealthCheckExcludedNames.addAll(inactiveNames);
inactiveOrHealthCheckExcludedNames.addAll(healthCheckExcludedNames);
this.inactiveNames = inactiveNames;
}
- // TODO careful when using this, as it might (incorrectly) not include the default datasource.
- // See TODO in code that calls the constructor of this class.
- // See https://github.com/quarkusio/quarkus/issues/37779
- public Set getConfiguredNames() {
- return configuredNames;
- }
-
public Set getInactiveNames() {
return inactiveNames;
}
diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesBuildTimeConfig.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesBuildTimeConfig.java
index eccf515ebc6c95..8074c5f18a286f 100644
--- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesBuildTimeConfig.java
+++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesBuildTimeConfig.java
@@ -5,7 +5,6 @@
import io.quarkus.datasource.common.runtime.DataSourceUtil;
import io.quarkus.runtime.annotations.ConfigDocMapKey;
-import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
@@ -22,7 +21,6 @@ public interface DataSourcesBuildTimeConfig {
/**
* Datasources.
*/
- @ConfigDocSection
@ConfigDocMapKey("datasource-name")
@WithParentName
@WithDefaults
diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesRuntimeConfig.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesRuntimeConfig.java
index 91e24d97721d7e..099b1aa4eaa06f 100644
--- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesRuntimeConfig.java
+++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesRuntimeConfig.java
@@ -4,7 +4,6 @@
import io.quarkus.datasource.common.runtime.DataSourceUtil;
import io.quarkus.runtime.annotations.ConfigDocMapKey;
-import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
@@ -19,7 +18,6 @@ public interface DataSourcesRuntimeConfig {
/**
* Datasources.
*/
- @ConfigDocSection
@ConfigDocMapKey("datasource-name")
@WithParentName
@WithDefaults
diff --git a/extensions/devservices/common/pom.xml b/extensions/devservices/common/pom.xml
index ef138c2628b2ff..c4a20804323064 100644
--- a/extensions/devservices/common/pom.xml
+++ b/extensions/devservices/common/pom.xml
@@ -31,6 +31,14 @@
+
+
+ commons-codec
+ commons-codec
+ io.quarkusquarkus-junit4-mock
diff --git a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java
index 39219be41eed6e..2488bcf52540e4 100644
--- a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java
+++ b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java
@@ -1,6 +1,5 @@
package io.quarkus.devservices.common;
-import java.net.URL;
import java.util.Map;
import org.testcontainers.containers.BindMode;
@@ -18,19 +17,12 @@ private Volumes() {
public static void addVolumes(GenericContainer> container, Map volumes) {
for (Map.Entry volume : volumes.entrySet()) {
String hostLocation = volume.getKey();
- BindMode bindMode = BindMode.READ_WRITE;
if (volume.getKey().startsWith(CLASSPATH)) {
- URL url = Thread.currentThread().getContextClassLoader()
- .getResource(hostLocation.replaceFirst(CLASSPATH, EMPTY));
- if (url == null) {
- throw new IllegalStateException("Classpath resource at '" + hostLocation + "' not found!");
- }
-
- hostLocation = url.getPath();
- bindMode = BindMode.READ_ONLY;
+ container.withClasspathResourceMapping(hostLocation.replaceFirst(CLASSPATH, EMPTY), volume.getValue(),
+ BindMode.READ_ONLY);
+ } else {
+ container.withFileSystemBind(hostLocation, volume.getValue(), BindMode.READ_WRITE);
}
-
- container.withFileSystemBind(hostLocation, volume.getValue(), bindMode);
}
}
}
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 6251dc5e96de03..9940177775761a 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
@@ -82,7 +82,7 @@ private static class QuarkusDb2Container extends Db2Container {
public QuarkusDb2Container(Optional imageName, OptionalInt fixedExposedPort, boolean useSharedNetwork) {
super(DockerImageName.parse(imageName.orElseGet(() -> ConfigureUtil.getDefaultImageNameFor("db2")))
- .asCompatibleSubstituteFor(DockerImageName.parse("ibmcom/db2")));
+ .asCompatibleSubstituteFor(DockerImageName.parse("icr.io/db2_community/db2")));
this.fixedExposedPort = fixedExposedPort;
this.useSharedNetwork = useSharedNetwork;
}
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 8fbbc469848234..eee0fdf9d98acd 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
@@ -181,29 +181,6 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch(
Distribution resolvedDistribution = resolveDistribution(config, buildItemConfig);
DockerImageName resolvedImageName = resolveImageName(config, resolvedDistribution);
- // Hibernate Search Elasticsearch have a version configuration property, we need to check that it is coherent
- // with the image we are about to launch
- if (buildItemConfig.version != null) {
- String containerTag = resolvedImageName.getVersionPart();
- if (!containerTag.startsWith(buildItemConfig.version)) {
- throw new BuildException(
- "Dev Services for Elasticsearch detected a version mismatch."
- + " Consuming extensions are configured to use version " + config.imageName
- + " but Dev Services are configured to use version " + buildItemConfig.version +
- ". Either configure the same version or disable Dev Services for Elasticsearch.",
- Collections.emptyList());
- }
- }
-
- if (buildItemConfig.distribution != null
- && !buildItemConfig.distribution.equals(resolvedDistribution)) {
- throw new BuildException(
- "Dev Services for Elasticsearch detected a distribution mismatch."
- + " Consuming extensions are configured to use distribution " + config.distribution
- + " but Dev Services are configured to use distribution " + buildItemConfig.distribution +
- ". Either configure the same distribution or disable Dev Services for Elasticsearch.",
- Collections.emptyList());
- }
final Optional maybeContainerAddress = elasticsearchContainerLocator.locateContainer(
config.serviceName,
@@ -214,8 +191,8 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch(
final Supplier defaultElasticsearchSupplier = () -> {
GenericContainer> container = resolvedDistribution.equals(Distribution.ELASTIC)
- ? createElasticsearchContainer(config, resolvedImageName)
- : createOpensearchContainer(config, resolvedImageName);
+ ? createElasticsearchContainer(config, resolvedImageName, useSharedNetwork)
+ : createOpensearchContainer(config, resolvedImageName, useSharedNetwork);
if (config.serviceName != null) {
container.withLabel(DEV_SERVICE_LABEL, config.serviceName);
@@ -247,27 +224,31 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch(
}
private GenericContainer> createElasticsearchContainer(ElasticsearchDevServicesBuildTimeConfig config,
- DockerImageName resolvedImageName) {
+ DockerImageName resolvedImageName, boolean useSharedNetwork) {
ElasticsearchContainer container = new ElasticsearchContainer(
resolvedImageName.asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch"));
- ConfigureUtil.configureSharedNetwork(container, DEV_SERVICE_ELASTICSEARCH);
+ if (useSharedNetwork) {
+ ConfigureUtil.configureSharedNetwork(container, DEV_SERVICE_ELASTICSEARCH);
+ }
// Disable security as else we would need to configure it correctly to avoid tons of WARNING in the log
container.addEnv("xpack.security.enabled", "false");
// Disable disk-based shard allocation thresholds:
// in a single-node setup they just don't make sense,
// and lead to problems on large disks with little space left.
- // See https://www.elastic.co/guide/en/elasticsearch/reference/8.9/modules-cluster.html#disk-based-shard-allocation
+ // See https://www.elastic.co/guide/en/elasticsearch/reference/8.12/modules-cluster.html#disk-based-shard-allocation
container.addEnv("cluster.routing.allocation.disk.threshold_enabled", "false");
container.addEnv("ES_JAVA_OPTS", config.javaOpts);
return container;
}
private GenericContainer> createOpensearchContainer(ElasticsearchDevServicesBuildTimeConfig config,
- DockerImageName resolvedImageName) {
+ DockerImageName resolvedImageName, boolean useSharedNetwork) {
OpensearchContainer container = new OpensearchContainer(
resolvedImageName.asCompatibleSubstituteFor("opensearchproject/opensearch"));
- ConfigureUtil.configureSharedNetwork(container, DEV_SERVICE_OPENSEARCH);
+ if (useSharedNetwork) {
+ ConfigureUtil.configureSharedNetwork(container, DEV_SERVICE_OPENSEARCH);
+ }
container.addEnv("bootstrap.memory_lock", "true");
container.addEnv("plugins.index_state_management.enabled", "false");
diff --git a/extensions/grpc/deployment/pom.xml b/extensions/grpc/deployment/pom.xml
index e5639e2ea0477c..5119c4215af59d 100644
--- a/extensions/grpc/deployment/pom.xml
+++ b/extensions/grpc/deployment/pom.xml
@@ -101,6 +101,11 @@
quarkus-elytron-security-properties-file-deploymenttest
+
+ me.escoffier.certs
+ certificate-generator-junit5
+ test
+
diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java
index 54f481ff16420e..e66fef8f2e7ef4 100644
--- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java
+++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java
@@ -26,6 +26,7 @@
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
@@ -345,16 +346,22 @@ public void transform(TransformationContext ctx) {
AnnotationInstance clientAnnotation = Annotations.find(ctx.getQualifiers(), GrpcDotNames.GRPC_CLIENT);
if (clientAnnotation != null && clientAnnotation.value() == null) {
String clientName = null;
+ AnnotationTarget annotationTarget = ctx.getTarget();
if (ctx.getTarget().kind() == Kind.FIELD) {
clientName = clientAnnotation.target().asField().name();
- } else if (ctx.getTarget().kind() == Kind.METHOD_PARAMETER) {
+ } else if (ctx.getTarget().kind() == Kind.METHOD
+ && clientAnnotation.target().kind().equals(Kind.METHOD_PARAMETER)) {
MethodParameterInfo param = clientAnnotation.target().asMethodParameter();
+ annotationTarget = param;
// We don't need to check if parameter names are recorded - that's validated elsewhere
clientName = param.method().parameterName(param.position());
}
if (clientName != null) {
ctx.transform().remove(GrpcDotNames::isGrpcClient)
- .add(GrpcDotNames.GRPC_CLIENT, AnnotationValue.createStringValue("value", clientName)).done();
+ .add(AnnotationInstance.builder(GrpcDotNames.GRPC_CLIENT)
+ .value(clientName)
+ .buildWithTarget(annotationTarget))
+ .done();
}
}
}
diff --git a/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js b/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js
index 27eba4c5a3594e..7a505a4861051d 100644
--- a/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js
+++ b/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js
@@ -2,6 +2,9 @@ import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
import { JsonRpc } from 'jsonrpc';
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js';
+import { observeState } from 'lit-element-state';
+import { themeState } from 'theme-state';
+import '@quarkus-webcomponents/codeblock';
import '@vaadin/progress-bar';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-sort-column.js';
@@ -9,14 +12,13 @@ import '@vaadin/vertical-layout';
import '@vaadin/tabs';
import '@vaadin/split-layout';
import 'qui-badge';
-import 'qui-code-block';
import 'qui-ide-link';
import '@vaadin/button';
/**
* This component shows the Grpc Services
*/
-export class QwcGrpcServices extends QwcHotReloadElement {
+export class QwcGrpcServices extends observeState(QwcHotReloadElement) {
jsonRpc = new JsonRpc(this);
streamsMap = new Map();
@@ -214,6 +216,7 @@ export class QwcGrpcServices extends QwcHotReloadElement {
mode='json'
content='${method.prototype}'
value='${method.prototype}'
+ theme='${themeState.theme.name}'
editable>
@@ -221,7 +224,8 @@ export class QwcGrpcServices extends QwcHotReloadElement {
+ content='\n\n\n\n'
+ theme='${themeState.theme.name}'>
`;
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/HelloWorldTlsEndpointTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/HelloWorldTlsEndpointTest.java
index d465032c4857c9..ec0acc2e3e07bb 100644
--- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/HelloWorldTlsEndpointTest.java
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/HelloWorldTlsEndpointTest.java
@@ -10,7 +10,11 @@
import io.quarkus.grpc.client.tls.HelloWorldTlsEndpoint;
import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "grpc-client-tls", formats = Format.PEM))
class HelloWorldTlsEndpointTest {
@RegisterExtension
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithJKSTrustStoreWithHttpServerTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithJKSTrustStoreWithHttpServerTest.java
new file mode 100644
index 00000000000000..0aa557be56e167
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithJKSTrustStoreWithHttpServerTest.java
@@ -0,0 +1,60 @@
+package io.quarkus.grpc.client.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.quarkus.grpc.GrpcClient;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class MtlsWithJKSTrustStoreWithHttpServerTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-jks.path=target/certs/grpc-client-truststore.jks
+ quarkus.grpc.clients.hello.tls.trust-certificate-jks.password=password
+ quarkus.grpc.clients.hello.tls.key-certificate-jks.path=target/certs/grpc-client-keystore.jks
+ quarkus.grpc.clients.hello.tls.key-certificate-jks.password=password
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.use-separate-server=false
+ quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.ssl.certificate.trust-store-file=target/certs/grpc-server-truststore.jks
+ quarkus.http.ssl.certificate.trust-store-password=password
+ quarkus.http.ssl.client-auth=REQUIRED
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @GrpcClient("hello")
+ GreeterGrpc.GreeterBlockingStub blockingHelloService;
+
+ @Test
+ void testClientTlsConfiguration() {
+ HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithP12TrustStoreWithHttpServerTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithP12TrustStoreWithHttpServerTest.java
new file mode 100644
index 00000000000000..458e93ccc47ddb
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithP12TrustStoreWithHttpServerTest.java
@@ -0,0 +1,60 @@
+package io.quarkus.grpc.client.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.quarkus.grpc.GrpcClient;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class MtlsWithP12TrustStoreWithHttpServerTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-p12.path=target/certs/grpc-client-truststore.p12
+ quarkus.grpc.clients.hello.tls.trust-certificate-p12.password=password
+ quarkus.grpc.clients.hello.tls.key-certificate-p12.path=target/certs/grpc-client-keystore.p12
+ quarkus.grpc.clients.hello.tls.key-certificate-p12.password=password
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.use-separate-server=false
+ quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.ssl.certificate.trust-store-file=target/certs/grpc-server-truststore.jks
+ quarkus.http.ssl.certificate.trust-store-password=password
+ quarkus.http.ssl.client-auth=REQUIRED
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @GrpcClient("hello")
+ GreeterGrpc.GreeterBlockingStub blockingHelloService;
+
+ @Test
+ void testClientTlsConfiguration() {
+ HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithPemTrustStoreWithHttpServerTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithPemTrustStoreWithHttpServerTest.java
new file mode 100644
index 00000000000000..e8a42172f0fffe
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/MtlsWithPemTrustStoreWithHttpServerTest.java
@@ -0,0 +1,59 @@
+package io.quarkus.grpc.client.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.quarkus.grpc.GrpcClient;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class MtlsWithPemTrustStoreWithHttpServerTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-pem.certs=target/certs/grpc-client-ca.crt
+ quarkus.grpc.clients.hello.tls.key-certificate-pem.certs=target/certs/grpc-client.crt
+ quarkus.grpc.clients.hello.tls.key-certificate-pem.keys=target/certs/grpc-client.key
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.use-separate-server=false
+ quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.ssl.certificate.trust-store-file=target/certs/grpc-server-truststore.jks
+ quarkus.http.ssl.certificate.trust-store-password=password
+ quarkus.http.ssl.client-auth=REQUIRED
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @GrpcClient("hello")
+ GreeterGrpc.GreeterBlockingStub blockingHelloService;
+
+ @Test
+ void testClientTlsConfiguration() {
+ HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithJKSTrustStoreTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithJKSTrustStoreTest.java
new file mode 100644
index 00000000000000..00adb5bbe3bc8e
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithJKSTrustStoreTest.java
@@ -0,0 +1,47 @@
+package io.quarkus.grpc.client.tls;
+
+import static io.restassured.RestAssured.get;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class TlsWithJKSTrustStoreTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.host=localhost
+ quarkus.grpc.clients.hello.port=9001
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-jks.path=target/certs/grpc-client-truststore.jks
+ quarkus.grpc.clients.hello.tls.trust-certificate-jks.password=password
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.ssl.certificate=target/certs/grpc.crt
+ quarkus.grpc.server.ssl.key=target/certs/grpc.key
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(io.grpc.examples.helloworld.GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @Test
+ void testClientTlsConfiguration() {
+ String response = get("/hello/blocking/neo").asString();
+ assertThat(response).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithJKSTrustStoreWithHttpServerTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithJKSTrustStoreWithHttpServerTest.java
new file mode 100644
index 00000000000000..851349cf67dc23
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithJKSTrustStoreWithHttpServerTest.java
@@ -0,0 +1,55 @@
+package io.quarkus.grpc.client.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.quarkus.grpc.GrpcClient;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class TlsWithJKSTrustStoreWithHttpServerTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-jks.path=target/certs/grpc-client-truststore.jks
+ quarkus.grpc.clients.hello.tls.trust-certificate-jks.password=password
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.use-separate-server=false
+ quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(io.grpc.examples.helloworld.GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @GrpcClient("hello")
+ GreeterGrpc.GreeterBlockingStub blockingHelloService;
+
+ @Test
+ void testClientTlsConfiguration() {
+ HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithP12TrustStoreTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithP12TrustStoreTest.java
new file mode 100644
index 00000000000000..c456376c9e9384
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithP12TrustStoreTest.java
@@ -0,0 +1,47 @@
+package io.quarkus.grpc.client.tls;
+
+import static io.restassured.RestAssured.get;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class TlsWithP12TrustStoreTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.host=localhost
+ quarkus.grpc.clients.hello.port=9001
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-p12.path=target/certs/grpc-client-truststore.p12
+ quarkus.grpc.clients.hello.tls.trust-certificate-p12.password=password
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.ssl.certificate=target/certs/grpc.crt
+ quarkus.grpc.server.ssl.key=target/certs/grpc.key
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(io.grpc.examples.helloworld.GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @Test
+ void testClientTlsConfiguration() {
+ String response = get("/hello/blocking/neo").asString();
+ assertThat(response).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithP12TrustStoreWithHttpServerTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithP12TrustStoreWithHttpServerTest.java
new file mode 100644
index 00000000000000..380c04b729d266
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithP12TrustStoreWithHttpServerTest.java
@@ -0,0 +1,55 @@
+package io.quarkus.grpc.client.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.quarkus.grpc.GrpcClient;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class TlsWithP12TrustStoreWithHttpServerTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-p12.path=target/certs/grpc-client-truststore.p12
+ quarkus.grpc.clients.hello.tls.trust-certificate-p12.password=password
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.use-separate-server=false
+ quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @GrpcClient("hello")
+ GreeterGrpc.GreeterBlockingStub blockingHelloService;
+
+ @Test
+ void testClientTlsConfiguration() {
+ HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithPemTrustStoreTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithPemTrustStoreTest.java
new file mode 100644
index 00000000000000..64a6a2682daf1a
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithPemTrustStoreTest.java
@@ -0,0 +1,46 @@
+package io.quarkus.grpc.client.tls;
+
+import static io.restassured.RestAssured.get;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class TlsWithPemTrustStoreTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.host=localhost
+ quarkus.grpc.clients.hello.port=9001
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-pem.certs=target/certs/grpc-client-ca.crt
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.ssl.certificate=target/certs/grpc.crt
+ quarkus.grpc.server.ssl.key=target/certs/grpc.key
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(io.grpc.examples.helloworld.GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @Test
+ void testClientTlsConfiguration() {
+ String response = get("/hello/blocking/neo").asString();
+ assertThat(response).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithPemTrustStoreWithHttpServerTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithPemTrustStoreWithHttpServerTest.java
new file mode 100644
index 00000000000000..ace79d838275dd
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/tls/TlsWithPemTrustStoreWithHttpServerTest.java
@@ -0,0 +1,54 @@
+package io.quarkus.grpc.client.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.quarkus.grpc.GrpcClient;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+class TlsWithPemTrustStoreWithHttpServerTest {
+
+ private static final String configuration = """
+ quarkus.grpc.clients.hello.plain-text=false
+ quarkus.grpc.clients.hello.tls.trust-certificate-pem.certs=target/certs/grpc-client-ca.crt
+ quarkus.grpc.clients.hello.tls.enabled=true
+ quarkus.grpc.clients.hello.use-quarkus-grpc-client=true
+
+ quarkus.grpc.server.use-separate-server=false
+ quarkus.grpc.server.plain-text=false # Force the client to use TLS for the tests
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(HelloWorldTlsEndpoint.class.getPackage())
+ .addPackage(GreeterGrpc.class.getPackage())
+ .add(new StringAsset(configuration), "application.properties"));
+
+ @GrpcClient("hello")
+ GreeterGrpc.GreeterBlockingStub blockingHelloService;
+
+ @Test
+ void testClientTlsConfiguration() {
+ HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java
index edcf342c1f61ca..5b66f4da9fe3d0 100644
--- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java
@@ -1,5 +1,6 @@
package io.quarkus.grpc.server;
+import java.io.File;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@@ -31,11 +32,16 @@
import io.quarkus.grpc.server.services.MutinyHelloService;
import io.quarkus.grpc.server.services.MutinyTestService;
import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
/**
* Test services exposed by the gRPC server implemented using the regular gRPC model.
* Communication uses TLS.
*/
+@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "grpc-tls", password = "wibble", formats = {
+ Format.JKS, Format.PEM, Format.PKCS12 }))
public class MutinyGrpcServiceWithSSLTest extends GrpcServiceTestBase {
@RegisterExtension
@@ -54,7 +60,7 @@ public class MutinyGrpcServiceWithSSLTest extends GrpcServiceTestBase {
@BeforeEach
public void init() throws Exception {
SslContext sslcontext = GrpcSslContexts.forClient()
- .trustManager(createTrustAllTrustManager())
+ .trustManager(new File("target/certs/grpc-tls-ca.crt"))
.build();
channel = NettyChannelBuilder.forAddress("localhost", 9001)
.sslContext(sslcontext)
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java
index 33c0f848933630..6509bcf88dd44c 100644
--- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java
@@ -30,11 +30,16 @@
import io.quarkus.grpc.server.services.HelloService;
import io.quarkus.grpc.server.services.TestService;
import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
/**
* Test services exposed by the gRPC server implemented using the regular gRPC model.
* Communication uses TLS and the key is loaded from the classpath.
*/
+@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "grpc-tls", password = "wibble", formats = {
+ Format.JKS, Format.PEM, Format.PKCS12 }))
public class RegularGrpcServiceWithSSLFromClasspathTest extends GrpcServiceTestBase {
@RegisterExtension
@@ -46,14 +51,14 @@ public class RegularGrpcServiceWithSSLFromClasspathTest extends GrpcServiceTestB
HelloRequestOrBuilder.class, HelloReplyOrBuilder.class,
EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class,
TestServiceGrpc.class)
- .addAsResource(new File("src/test/resources/tls/server-keystore.jks"), "server-keystore.jks"))
+ .addAsResource(new File("target/certs/grpc-tls-keystore.jks"), "server-keystore.jks"))
.withConfigurationResource("grpc-server-tls-classpath-configuration.properties");
@Override
@BeforeEach
public void init() throws Exception {
SslContext sslcontext = GrpcSslContexts.forClient()
- .trustManager(createTrustAllTrustManager())
+ .trustManager(new File("target/certs/grpc-tls-ca.crt"))
.build();
channel = NettyChannelBuilder.forAddress("localhost", 9001)
.sslContext(sslcontext)
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java
index 835b0dd868d703..3b22054f041b3b 100644
--- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java
@@ -1,5 +1,6 @@
package io.quarkus.grpc.server;
+import java.io.File;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@@ -29,11 +30,16 @@
import io.quarkus.grpc.server.services.HelloService;
import io.quarkus.grpc.server.services.TestService;
import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
/**
* Test services exposed by the gRPC server implemented using the regular gRPC model.
* Communication uses TLS.
*/
+@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "grpc-tls", password = "wibble", formats = {
+ Format.JKS, Format.PEM, Format.PKCS12 }))
public class RegularGrpcServiceWithSSLTest extends GrpcServiceTestBase {
@RegisterExtension
@@ -52,7 +58,7 @@ public class RegularGrpcServiceWithSSLTest extends GrpcServiceTestBase {
@BeforeEach
public void init() throws Exception {
SslContext sslcontext = GrpcSslContexts.forClient()
- .trustManager(createTrustAllTrustManager())
+ .trustManager(new File("target/certs/grpc-tls-ca.crt"))
.build();
channel = NettyChannelBuilder.forAddress("localhost", 9001)
.sslContext(sslcontext)
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingJKSTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingJKSTest.java
new file mode 100644
index 00000000000000..36fed74b5500e7
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingJKSTest.java
@@ -0,0 +1,76 @@
+package io.quarkus.grpc.server.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.ManagedChannel;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyChannelBuilder;
+import io.netty.handler.ssl.SslContext;
+import io.quarkus.grpc.server.services.HelloService;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+public class TlsWithHttpServerUsingJKSTest {
+
+ static String configuration = """
+ quarkus.grpc.server.use-separate-server=false
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.jks
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(GreeterGrpc.class.getPackage())
+ .addClass(HelloService.class)
+ .add(new StringAsset(configuration), "application.properties"));
+
+ protected ManagedChannel channel;
+
+ @BeforeEach
+ public void init() throws Exception {
+ File certs = new File("target/certs/grpc-client-ca.crt");
+ SslContext sslcontext = GrpcSslContexts.forClient()
+ .trustManager(certs)
+ .build();
+ channel = NettyChannelBuilder.forAddress("localhost", 8444)
+ .sslContext(sslcontext)
+ .useTransportSecurity()
+ .build();
+ }
+
+ @AfterEach
+ public void shutdown() {
+ if (channel != null) {
+ channel.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testInvokingGrpcServiceUsingTls() {
+ HelloReply reply = GreeterGrpc.newBlockingStub(channel)
+ .sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingP12Test.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingP12Test.java
new file mode 100644
index 00000000000000..57ee009e332be9
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingP12Test.java
@@ -0,0 +1,76 @@
+package io.quarkus.grpc.server.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.ManagedChannel;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyChannelBuilder;
+import io.netty.handler.ssl.SslContext;
+import io.quarkus.grpc.server.services.HelloService;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+public class TlsWithHttpServerUsingP12Test {
+
+ static String configuration = """
+ quarkus.grpc.server.use-separate-server=false
+
+ quarkus.http.ssl.certificate.key-store-file=target/certs/grpc-keystore.p12
+ quarkus.http.ssl.certificate.key-store-password=password
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(GreeterGrpc.class.getPackage())
+ .addClass(HelloService.class)
+ .add(new StringAsset(configuration), "application.properties"));
+
+ protected ManagedChannel channel;
+
+ @BeforeEach
+ public void init() throws Exception {
+ File certs = new File("target/certs/grpc-client-ca.crt");
+ SslContext sslcontext = GrpcSslContexts.forClient()
+ .trustManager(certs)
+ .build();
+ channel = NettyChannelBuilder.forAddress("localhost", 8444)
+ .sslContext(sslcontext)
+ .useTransportSecurity()
+ .build();
+ }
+
+ @AfterEach
+ public void shutdown() {
+ if (channel != null) {
+ channel.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testInvokingGrpcServiceUsingTls() {
+ HelloReply reply = GreeterGrpc.newBlockingStub(channel)
+ .sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingPemTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingPemTest.java
new file mode 100644
index 00000000000000..0b1b19cad66dbe
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithHttpServerUsingPemTest.java
@@ -0,0 +1,76 @@
+package io.quarkus.grpc.server.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.ManagedChannel;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyChannelBuilder;
+import io.netty.handler.ssl.SslContext;
+import io.quarkus.grpc.server.services.HelloService;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+public class TlsWithHttpServerUsingPemTest {
+
+ static String configuration = """
+ quarkus.grpc.server.use-separate-server=false
+
+ quarkus.http.ssl.certificate.files=target/certs/grpc.crt
+ quarkus.http.ssl.certificate.key-files=target/certs/grpc.key
+ quarkus.http.insecure-requests=disabled
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(GreeterGrpc.class.getPackage())
+ .addClass(HelloService.class)
+ .add(new StringAsset(configuration), "application.properties"));
+
+ protected ManagedChannel channel;
+
+ @BeforeEach
+ public void init() throws Exception {
+ File certs = new File("target/certs/grpc-client-ca.crt");
+ SslContext sslcontext = GrpcSslContexts.forClient()
+ .trustManager(certs)
+ .build();
+ channel = NettyChannelBuilder.forAddress("localhost", 8444)
+ .sslContext(sslcontext)
+ .useTransportSecurity()
+ .build();
+ }
+
+ @AfterEach
+ public void shutdown() {
+ if (channel != null) {
+ channel.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testInvokingGrpcServiceUsingTls() {
+ HelloReply reply = GreeterGrpc.newBlockingStub(channel)
+ .sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithJksKeyStoreTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithJksKeyStoreTest.java
new file mode 100644
index 00000000000000..17097fff7bed92
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithJksKeyStoreTest.java
@@ -0,0 +1,74 @@
+package io.quarkus.grpc.server.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.*;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyChannelBuilder;
+import io.netty.handler.ssl.SslContext;
+import io.quarkus.grpc.server.services.HelloService;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+public class TlsWithJksKeyStoreTest {
+
+ static String configuration = """
+ quarkus.grpc.server.ssl.key-store=target/certs/grpc-keystore.jks
+ quarkus.grpc.server.ssl.key-store-password=password
+ quarkus.grpc.server.alpn=true
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(GreeterGrpc.class.getPackage())
+ .addClass(HelloService.class)
+ .add(new StringAsset(configuration), "application.properties"));
+
+ protected ManagedChannel channel;
+
+ @BeforeEach
+ public void init() throws Exception {
+ File certs = new File("target/certs/grpc-client-ca.crt");
+ SslContext sslcontext = GrpcSslContexts.forClient()
+ .trustManager(certs)
+ .build();
+ channel = NettyChannelBuilder.forAddress("localhost", 9001)
+ .sslContext(sslcontext)
+ .useTransportSecurity()
+ .build();
+ }
+
+ @AfterEach
+ public void shutdown() {
+ if (channel != null) {
+ channel.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testInvokingGrpcServiceUsingTls() {
+ HelloReply reply = GreeterGrpc.newBlockingStub(channel)
+ .sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithP12KeyStoreTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithP12KeyStoreTest.java
new file mode 100644
index 00000000000000..4a566a941c04a2
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithP12KeyStoreTest.java
@@ -0,0 +1,74 @@
+package io.quarkus.grpc.server.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.ManagedChannel;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyChannelBuilder;
+import io.netty.handler.ssl.SslContext;
+import io.quarkus.grpc.server.services.HelloService;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+public class TlsWithP12KeyStoreTest {
+
+ static String configuration = """
+ quarkus.grpc.server.ssl.key-store=target/certs/grpc-keystore.p12
+ quarkus.grpc.server.ssl.key-store-password=password
+ quarkus.grpc.server.alpn=true
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(GreeterGrpc.class.getPackage())
+ .addClass(HelloService.class)
+ .add(new StringAsset(configuration), "application.properties"));
+
+ protected ManagedChannel channel;
+
+ @BeforeEach
+ public void init() throws Exception {
+ File certs = new File("target/certs/grpc-client-ca.crt");
+ SslContext sslcontext = GrpcSslContexts.forClient()
+ .trustManager(certs)
+ .build();
+ channel = NettyChannelBuilder.forAddress("localhost", 9001)
+ .sslContext(sslcontext)
+ .useTransportSecurity()
+ .build();
+ }
+
+ @AfterEach
+ public void shutdown() {
+ if (channel != null) {
+ channel.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testInvokingGrpcServiceUsingTls() {
+ HelloReply reply = GreeterGrpc.newBlockingStub(channel)
+ .sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+
+}
diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithPemKeyStoreTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithPemKeyStoreTest.java
new file mode 100644
index 00000000000000..dc19a4b22a1463
--- /dev/null
+++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/tls/TlsWithPemKeyStoreTest.java
@@ -0,0 +1,74 @@
+package io.quarkus.grpc.server.tls;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.grpc.ManagedChannel;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyChannelBuilder;
+import io.netty.handler.ssl.SslContext;
+import io.quarkus.grpc.server.services.HelloService;
+import io.quarkus.test.QuarkusUnitTest;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certs", certificates = {
+ @Certificate(name = "grpc", password = "password", formats = { Format.JKS, Format.PEM, Format.PKCS12 }, client = true)
+})
+public class TlsWithPemKeyStoreTest {
+
+ static String configuration = """
+ quarkus.grpc.server.ssl.certificate=target/certs/grpc.crt
+ quarkus.grpc.server.ssl.key=target/certs/grpc.key
+ quarkus.grpc.server.alpn=true
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addPackage(GreeterGrpc.class.getPackage())
+ .addClass(HelloService.class)
+ .add(new StringAsset(configuration), "application.properties"));
+
+ protected ManagedChannel channel;
+
+ @BeforeEach
+ public void init() throws Exception {
+ File certs = new File("target/certs/grpc-client-ca.crt");
+ SslContext sslcontext = GrpcSslContexts.forClient()
+ .trustManager(certs)
+ .build();
+ channel = NettyChannelBuilder.forAddress("localhost", 9001)
+ .sslContext(sslcontext)
+ .useTransportSecurity()
+ .build();
+ }
+
+ @AfterEach
+ public void shutdown() {
+ if (channel != null) {
+ channel.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testInvokingGrpcServiceUsingTls() {
+ HelloReply reply = GreeterGrpc.newBlockingStub(channel)
+ .sayHello(HelloRequest.newBuilder().setName("neo").build());
+ assertThat(reply.getMessage()).isEqualTo("Hello neo");
+ }
+
+}
diff --git a/extensions/grpc/deployment/src/test/resources/grpc-client-tls-configuration.properties b/extensions/grpc/deployment/src/test/resources/grpc-client-tls-configuration.properties
index c393ab7d3fd1ef..ba7bb700876412 100644
--- a/extensions/grpc/deployment/src/test/resources/grpc-client-tls-configuration.properties
+++ b/extensions/grpc/deployment/src/test/resources/grpc-client-tls-configuration.properties
@@ -1,6 +1,6 @@
quarkus.grpc.clients.hello.host=localhost
quarkus.grpc.clients.hello.port=9001
-quarkus.grpc.clients.hello.ssl.trust-store=src/test/resources/tls-from-file/ca.pem
+quarkus.grpc.clients.hello.ssl.trust-store=target/certs/grpc-client-tls-ca.crt
-quarkus.grpc.server.ssl.certificate=src/test/resources/tls-from-file/server.pem
-quarkus.grpc.server.ssl.key=src/test/resources/tls-from-file/server.key
\ No newline at end of file
+quarkus.grpc.server.ssl.certificate=target/certs/grpc-client-tls.crt
+quarkus.grpc.server.ssl.key=target/certs/grpc-client-tls.key
\ No newline at end of file
diff --git a/extensions/grpc/deployment/src/test/resources/grpc-server-tls-configuration.properties b/extensions/grpc/deployment/src/test/resources/grpc-server-tls-configuration.properties
index b7d56ade9f0152..57700bf9e33c83 100644
--- a/extensions/grpc/deployment/src/test/resources/grpc-server-tls-configuration.properties
+++ b/extensions/grpc/deployment/src/test/resources/grpc-server-tls-configuration.properties
@@ -1,3 +1,3 @@
-quarkus.grpc.server.ssl.key-store=src/test/resources/tls/server-keystore.jks
+quarkus.grpc.server.ssl.key-store=target/certs/grpc-tls-keystore.jks
quarkus.grpc.server.ssl.key-store-password=wibble
quarkus.grpc.server.alpn=true
diff --git a/extensions/grpc/deployment/src/test/resources/tls-from-file/README.md b/extensions/grpc/deployment/src/test/resources/tls-from-file/README.md
deleted file mode 100644
index 8c1886e98b5e4d..00000000000000
--- a/extensions/grpc/deployment/src/test/resources/tls-from-file/README.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Generating the certificates and keys
-
-The ca is self-signed:
-----------------------
-
-```bash
-openssl req -x509 -new -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem \
- -config ca-openssl.cnf -days 3650 -extensions v3_req
-```
-
-When prompted for certificate information, everything is default.
-
-Client is issued by CA:
------------------------
-
-```bash
-openssl genrsa -out client.key.rsa 2048
-openssl pkcs8 -topk8 -in client.key.rsa -out client.key -nocrypt
-openssl req -new -key client.key -out client.csr
-```
-
-When prompted for certificate information, everything is default except the
-common name which is set to `testclient`.
-
-```bash
-openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial -in client.csr \
- -out client.pem -days 3650
-```
-
-server is issued by CA with a special config for subject alternative names:
-----------------------------------------------------------------------------
-
-```bash
-openssl genrsa -out server1.key.rsa 2048
-openssl pkcs8 -topk8 -in server.key.rsa -out server.key -nocrypt
-openssl req -new -key server.key -out server.csr -config server-openssl.cnf
-```
-
-When prompted for certificate information, everything is default except the
-common name which is set to `localhost`.
-
-```bash
-openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial -in server.csr \
- -out server.pem -extfile server-openssl.cnf -days 3650
-```
-
-Cleanup
--------
-
-```bash
-rm *.rsa
-rm *.csr
-rm ca.srl
-```
\ No newline at end of file
diff --git a/extensions/grpc/deployment/src/test/resources/tls-from-file/ca-openssl.cnf b/extensions/grpc/deployment/src/test/resources/tls-from-file/ca-openssl.cnf
deleted file mode 100644
index 7a8528ec2304a1..00000000000000
--- a/extensions/grpc/deployment/src/test/resources/tls-from-file/ca-openssl.cnf
+++ /dev/null
@@ -1,17 +0,0 @@
-[req]
-distinguished_name = req_distinguished_name
-req_extensions = v3_req
-
-[req_distinguished_name]
-countryName = Country Name (2 letter code)
-countryName_default = FR
-stateOrProvinceName = State or Province Name (full name)
-stateOrProvinceName_default = Some-State
-organizationName = Organization Name (eg, company)
-organizationName_default = Acme Corp
-commonName = Common Name (eg, YOUR name)
-commonName_default = testca
-
-[v3_req]
-basicConstraints = CA:true
-keyUsage = critical, keyCertSign
\ No newline at end of file
diff --git a/extensions/grpc/deployment/src/test/resources/tls-from-file/ca.pem b/extensions/grpc/deployment/src/test/resources/tls-from-file/ca.pem
deleted file mode 100644
index b3c622bc96b47c..00000000000000
--- a/extensions/grpc/deployment/src/test/resources/tls-from-file/ca.pem
+++ /dev/null
@@ -1,20 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDMTCCAhmgAwIBAgIJAKH9BBbnY/fjMA0GCSqGSIb3DQEBCwUAMEcxCzAJBgNV
-BAYTAkZSMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQKDAlBY21lIENvcnAx
-DzANBgNVBAMMBnRlc3RjYTAeFw0yMDA0MDQwNzI5NTdaFw0zMDA0MDIwNzI5NTda
-MEcxCzAJBgNVBAYTAkZSMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQKDAlB
-Y21lIENvcnAxDzANBgNVBAMMBnRlc3RjYTCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBAJqJz93r5o5q+yr2zX97CZaW+6pqVhdf8x73x+/E+9fIML96pvUJ
-VQJSMVV+dOHqb860inw9WE7itckTlArB5vpVfjGunbCGOxNTgaUBMJeaalXgFUkL
-36mFpGhwXVaUVKdrCSbol3R/eKAc6qcN4g/plKgSnGtODjxd1za64OhZn3Cz8tUN
-yxmnGlFkOU7S3F8YF2aTmIjZZcs7JF93VmLyVzDkWzlySmKqXXt1xpzFADCMzHjR
-GuUaOilC9dstvfZHNhQv2TIBYHwltShYV/86J+p5RXlRkaHxfnqKgPenw82SehWs
-2eGkgLFnUp1Q9nTwIB4l7NmEyJzzTg0xMj8CAwEAAaMgMB4wDAYDVR0TBAUwAwEB
-/zAOBgNVHQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAAxVmtqKlPaXOIDv
-ZHaLLt0TdhukEqf13nMPSb6sxA0eOD3GNR3CUBMfftVRkEVjvqZRbYjJTnG9IW8E
-39tESrLPnoWEYdvDJBM4sTsG2JLVpaq6mNcMQABkAIjnbjq0yXK+WwP6Ug5HD7Ds
-r8hbiu5T039v0uHxJOM/+nhkyyHMsAYaAnUKQ9F0fbo8Jp/d5KK70k5hMpb2smr1
-cYVkN9AZlBgOeciFfO1RrWGsnTo3Aln6wciSXPqqj42t8WZJp1s11Lqcml1zJqHe
-dQkI9NjPZ67V0D0mytvIixchClMCl9E9AkXjGdapE1lQ3s6TK5t0COpo0VE4KAvd
-cXRzvCI=
------END CERTIFICATE-----
diff --git a/extensions/grpc/deployment/src/test/resources/tls-from-file/server-openssl.cnf b/extensions/grpc/deployment/src/test/resources/tls-from-file/server-openssl.cnf
deleted file mode 100644
index 3962a52d790d4b..00000000000000
--- a/extensions/grpc/deployment/src/test/resources/tls-from-file/server-openssl.cnf
+++ /dev/null
@@ -1,80 +0,0 @@
-[req]
-distinguished_name = req_distinguished_name
-req_extensions = v3_req
-
-[req_distinguished_name]
-countryName = Country Name (2 letter code)
-countryName_default = FR
-stateOrProvinceName = State or Province Name (full name)
-stateOrProvinceName_default = Some-State
-localityName = Locality Name (eg, city)
-localityName_default = Valence
-organizationName = Organization Name (eg, company)
-organizationName_default = Acme Corp
-commonName = Common Name (eg, YOUR name)
-commonName_max = 64
-
-####################################################################
-[ ca ]
-default_ca = CA_default # The default ca section
-
-####################################################################
-[ CA_default ]
-
-dir = . # Where everything is kept
-certs = $dir # Where the issued certs are kept
-crl_dir = $dir # Where the issued crl are kept
-database = $dir/index.txt # database index file.
-new_certs_dir = $dir # default place for new certs.
-
-certificate = $dir/ca.pem # The CA certificate
-serial = $dir/serial # The current serial number
-crlnumber = $dir/crlnumber # the current crl number
- # must be commented out to leave a V1 CRL
-crl = $dir/crl.pem # The current CRL
-private_key = $dir/private/cakey.pem# The private key
-RANDFILE = $dir/private/.rand # private random number file
-
-x509_extensions = usr_cert
-
-# Comment out the following two lines for the "traditional"
-# (and highly broken) format.
-name_opt = ca_default # Subject Name options
-cert_opt = ca_default # Certificate field options
-
-# Extension copying option: use with caution.
-# copy_extensions = copy
-
-# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs
-# so this is commented out by default to leave a V1 CRL.
-# crlnumber must also be commented out to leave a V1 CRL.
-# crl_extensions = crl_ext
-
-default_days = 365 # how long to certify for
-default_crl_days= 30 # how long before next CRL
-default_md = default # use public key default MD
-preserve = no # keep passed DN ordering
-
-# A few difference way of specifying how similar the request should look
-# For type CA, the listed attributes must be the same, and the optional
-# and supplied fields are just that :-)
-policy = policy_anything
-[ policy_anything ]
-countryName = optional
-stateOrProvinceName = optional
-localityName = optional
-organizationName = optional
-organizationalUnitName = optional
-commonName = supplied
-emailAddress = optional
-
-[v3_req]
-basicConstraints = CA:FALSE
-keyUsage = nonRepudiation, digitalSignature, keyEncipherment
-subjectAltName = @alt_names
-
-[alt_names]
-DNS.1 = localhost
-DNS.2 = *.test.com
-IP.1 = "127.0.0.1"
-IP.2 = "192.168.1.3"
\ No newline at end of file
diff --git a/extensions/grpc/deployment/src/test/resources/tls-from-file/server.key b/extensions/grpc/deployment/src/test/resources/tls-from-file/server.key
deleted file mode 100644
index b49d98c926e61c..00000000000000
--- a/extensions/grpc/deployment/src/test/resources/tls-from-file/server.key
+++ /dev/null
@@ -1,28 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCy40A8Keuwa6aK
-NRxqa5nbrNpQnWF1hd9kO848UzUI06X9GLve459Q4KCNUDgUA1PL6zUaVbFpmLrp
-6iBimxr1u+HqVyYkha4veLXKA1+nLcp0gUOQTb4Dk6jHdGhgwIaPo8saFO9sY2Vy
-+xh8MU8cepjZtxgt9cI/+RGQgg0uEz7NT3tjlX3b7Y7SQChv2fsA6ZuE/3v2XBLg
-zCgNEfdYh46B0pkE3Od3xq+cieOOuFHCeHFX+wZcCJWHa7tLxsgKIp+QohqG4MoF
-KWCf7484nu8EYd7YzNdzPN6XmcQuw2IUr2YjDFaY0EZ6ERTkSRoMBbvjq9bGPYix
-kPHwQnOFAgMBAAECggEBAJP7yJy8tRvpwgidLRegUdRXZva/aus0xvt9OfvPfZUC
-uVLpzijxtk7KtCaS0QFFS2Hq/q/9admIHj/5jbbkxuW3+ojIdWZLLDBbNE+cgNmk
-2NGOSZ0rouAEm9/8HYjEW8yh3BeEcBgDFd6Ld9LtW5uck6uvepIytvIDEhOwckTF
-BtrtJubHIAjeTFfdikcNuQbQHwWDWu8ykRG7gXyEGXJjWXV+lRZqY+JtBm8TpF2H
-6pj1DwPcBt0yt7LoXrrfIlMabqdWXgmfu+j5kvxyZNWOg56G48hfFIg3e8dri/7/
-xHI8t3dnN/L67VbHMor3xMYmeGQQnq9Zkv5tcj+c40ECgYEA2Q5svQA66l6ubpJo
-sSKfBIskIe/WuzWB0YmV0jdg0mqivG/bxObK0sZr6Jcd4T3v5J3PMit0G3gOfhPO
-sD6otk/Lo9d7whOfskjG3kDODrx+mRVHhVfamUcqzk722OrDGh9NeCHaCbPBAhMd
-tmugi+SF1eSUPtfAwqW9o0qP7NUCgYEA0vu1gOxFZPbIdThNIGyzudn1+FSpEc67
-kiB/FJnculpPf9fRaL//dMG+sItUwFvSTJ+xYf+mRcSXl6imY7zSTgakjAp+mzMZ
-LPZeMY9qZxEgIIiLcX/WVi6Ldveukh9Mbzd6v+dfqZJEBiA5EDfyjdVl6APRmybv
-3VR/rHoGA/ECgYEA12TMHWY8ENXyTUG26IkNukmFirPhqBd6AwCAj4Jq74PoiAyM
-z0Gj5jQHY2GKwjlfdMPxl7ytVJD3+L8ZLaaQb5KR573vTvGAWUCFMIqosND25FzM
-g5NiFxcbcG3F4g5dm++SRfN51oTttGxZ4Ou+/vPAqDhTsGUUIVSt8nwMhR0CgYAM
-iTQxothEtX0Xqe67PHo5UsAQr0cUbcorVo72dGXvFKqgl/wzUyUklNZ1uvGgNFR8
-hQiPIBeLEbFIK5cnWfLM/AwO3hjDs/eM+l6CZ1kVIqlcBYDzj3r2x/E1cmYG/KEY
-Ap3ihSbj9nLgQk3hrtFUqBdT/9YWd+vMpNapMt7koQKBgBNzxUtQOKcT3EaJytfe
-LqVayA6mfhjZisUtw1ufg9iPUDyGo8Wj3h5QI0aY9240Bztp/f/qWRF0vCcA6mNm
-U4edOI1EjWqvgkLw0Nav3xSNO4jDchA/A9Lj2y6mjGmmaj5E17vowRz1s3GHDtIX
-lpwir++lc7qUgPNttzfK/ltI
------END PRIVATE KEY-----
diff --git a/extensions/grpc/deployment/src/test/resources/tls-from-file/server.pem b/extensions/grpc/deployment/src/test/resources/tls-from-file/server.pem
deleted file mode 100644
index 0564aead544636..00000000000000
--- a/extensions/grpc/deployment/src/test/resources/tls-from-file/server.pem
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDJDCCAgygAwIBAgIJAKhxPs8iIgiBMA0GCSqGSIb3DQEBBQUAMEcxCzAJBgNV
-BAYTAkZSMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQKDAlBY21lIENvcnAx
-DzANBgNVBAMMBnRlc3RjYTAeFw0yMDA0MDQwODEzMjhaFw0zMDA0MDIwODEzMjha
-MFwxCzAJBgNVBAYTAkZSMRMwEQYDVQQIDApTb21lLVN0YXRlMRAwDgYDVQQHDAdW
-YWxlbmNlMRIwEAYDVQQKDAlBY21lIENvcnAxEjAQBgNVBAMMCWxvY2FsaG9zdDCC
-ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALLjQDwp67Brpoo1HGprmdus
-2lCdYXWF32Q7zjxTNQjTpf0Yu97jn1DgoI1QOBQDU8vrNRpVsWmYuunqIGKbGvW7
-4epXJiSFri94tcoDX6ctynSBQ5BNvgOTqMd0aGDAho+jyxoU72xjZXL7GHwxTxx6
-mNm3GC31wj/5EZCCDS4TPs1Pe2OVfdvtjtJAKG/Z+wDpm4T/e/ZcEuDMKA0R91iH
-joHSmQTc53fGr5yJ4464UcJ4cVf7BlwIlYdru0vGyAoin5CiGobgygUpYJ/vjzie
-7wRh3tjM13M83peZxC7DYhSvZiMMVpjQRnoRFORJGgwFu+Or1sY9iLGQ8fBCc4UC
-AwEAATANBgkqhkiG9w0BAQUFAAOCAQEAFEYYp3wV1xH9+Z44GJSjTa9Ltg5gxqmf
-t7ROsiy01vPuWQDhLILRpGQHdyz07AOmYsaGRTMOCcOsUCDY7u0NYi991HBEn1ZI
-/+2tFNZnLTk/hLtNSW30MWt0sNE2d6DIMZQBAtvrWnFdrQAThzTQLeFpbi+nTX+O
-KwtVpci8gMej90fEnyYRdNn/yTmjo79Q4+yuD9oKhvsOxy65CmSvXh9oLpqH7HzQ
-TR6LfuWSpG6wGKy1JW5fXuwb0YJC1QjvAOVWr9zm79wyeXUuXfQ6A2SNHTPwwEbu
-K+kV+UX7NofWhepi1QOxKnnO3EXbdN7fl44jEuzaGOKtmfhiRse4cQ==
------END CERTIFICATE-----
diff --git a/extensions/grpc/deployment/src/test/resources/tls/client-truststore.jks b/extensions/grpc/deployment/src/test/resources/tls/client-truststore.jks
deleted file mode 100644
index fbb345f0cd3bb9..00000000000000
Binary files a/extensions/grpc/deployment/src/test/resources/tls/client-truststore.jks and /dev/null differ
diff --git a/extensions/grpc/deployment/src/test/resources/tls/server-keystore.jks b/extensions/grpc/deployment/src/test/resources/tls/server-keystore.jks
deleted file mode 100644
index 8e68758e08cbce..00000000000000
Binary files a/extensions/grpc/deployment/src/test/resources/tls/server-keystore.jks and /dev/null differ
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
index f11186d3a30a65..2f95cf0248e87d 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
@@ -186,7 +186,7 @@ private void buildGrpcServer(Vertx vertx, GrpcServerConfiguration configuration,
initHealthStorage();
- LOGGER.info("Starting new Vert.x gRPC server ...");
+ LOGGER.info("Starting new Quarkus gRPC server (using Vert.x transport)...");
Route route = routerSupplier.getValue().route().handler(ctx -> {
if (!isGrpc(ctx)) {
ctx.next();
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcClientConfiguration.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcClientConfiguration.java
index 16f1a0540f4090..80a9092fcb07b9 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcClientConfiguration.java
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcClientConfiguration.java
@@ -60,9 +60,16 @@ public class GrpcClientConfiguration {
/**
* The SSL/TLS config.
+ * Only use this if you want to use the old Java gRPC client.
*/
public SslClientConfig ssl;
+ /**
+ * The TLS config.
+ * Only use this if you want to use the Quarkus gRPC client.
+ */
+ public TlsClientConfig tls;
+
/**
* Use a name resolver. Defaults to dns.
* If set to "stork", host will be treated as SmallRye Stork service name
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/TlsClientConfig.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/TlsClientConfig.java
new file mode 100644
index 00000000000000..a48f9786b6c214
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/TlsClientConfig.java
@@ -0,0 +1,132 @@
+package io.quarkus.grpc.runtime.config;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+@ConfigGroup
+public class TlsClientConfig {
+
+ /**
+ * Whether SSL/TLS is enabled.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean enabled;
+
+ /**
+ * Enable trusting all certificates. Disabled by default.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean trustAll;
+
+ /**
+ * Trust configuration in the PEM format.
+ *
+ * When used, {@code trust-certificate-jks} and {@code trust-certificate-p12} must not be used.
+ */
+ public PemTrustCertConfiguration trustCertificatePem;
+
+ /**
+ * Trust configuration in the JKS format.
+ *
+ * When configured, {@code trust-certificate-pem} and {@code trust-certificate-p12} must not be used.
+ */
+ public JksConfiguration trustCertificateJks;
+
+ /**
+ * Trust configuration in the P12 format.
+ *
+ * When configured, {@code trust-certificate-jks} and {@code trust-certificate-pem} must not be used.
+ */
+ public PfxConfiguration trustCertificateP12;
+
+ /**
+ * Key/cert configuration in the PEM format.
+ *
+ * When configured, {@code key-certificate-jks} and {@code key-certificate-p12} must not be used.
+ */
+ public PemKeyCertConfiguration keyCertificatePem;
+
+ /**
+ * Key/cert configuration in the JKS format.
+ *
+ * When configured, {@code #key-certificate-pem} and {@code #key-certificate-p12} must not be used.
+ */
+ public JksConfiguration keyCertificateJks;
+
+ /**
+ * Key/cert configuration in the P12 format.
+ *
+ * When configured, {@code key-certificate-jks} and {@code #key-certificate-pem} must not be used.
+ */
+ public PfxConfiguration keyCertificateP12;
+
+ /**
+ * Whether hostname should be verified in the SSL/TLS handshake.
+ */
+ @ConfigItem(defaultValue = "true")
+ public boolean verifyHostname;
+
+ @ConfigGroup
+ public static class PemTrustCertConfiguration {
+
+ /**
+ * Comma-separated list of the trust certificate files (Pem format).
+ */
+ @ConfigItem
+ public Optional> certs;
+
+ }
+
+ @ConfigGroup
+ public static class JksConfiguration {
+
+ /**
+ * Path of the key file (JKS format).
+ */
+ @ConfigItem
+ public Optional path;
+
+ /**
+ * Password of the key file.
+ */
+ @ConfigItem
+ public Optional password;
+ }
+
+ @ConfigGroup
+ public static class PfxConfiguration {
+
+ /**
+ * Path to the key file (PFX format).
+ */
+ @ConfigItem
+ public Optional path;
+
+ /**
+ * Password of the key.
+ */
+ @ConfigItem
+ public Optional password;
+ }
+
+ @ConfigGroup
+ public static class PemKeyCertConfiguration {
+
+ /**
+ * Comma-separated list of the path to the key files (Pem format).
+ */
+ @ConfigItem
+ public Optional> keys;
+
+ /**
+ * Comma-separated list of the path to the certificate files (Pem format).
+ */
+ @ConfigItem
+ public Optional> certs;
+
+ }
+
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/Channels.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/Channels.java
index 65831169c0b036..aafa71bdbb8807 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/Channels.java
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/Channels.java
@@ -5,6 +5,12 @@
import static io.grpc.netty.NettyChannelBuilder.DEFAULT_FLOW_CONTROL_WINDOW;
import static io.quarkus.grpc.runtime.GrpcTestPortUtils.testPort;
import static io.quarkus.grpc.runtime.config.GrpcClientConfiguration.DNS;
+import static io.quarkus.grpc.runtime.supports.SSLConfigHelper.configureJksKeyCertOptions;
+import static io.quarkus.grpc.runtime.supports.SSLConfigHelper.configureJksTrustOptions;
+import static io.quarkus.grpc.runtime.supports.SSLConfigHelper.configurePemKeyCertOptions;
+import static io.quarkus.grpc.runtime.supports.SSLConfigHelper.configurePemTrustOptions;
+import static io.quarkus.grpc.runtime.supports.SSLConfigHelper.configurePfxKeyCertOptions;
+import static io.quarkus.grpc.runtime.supports.SSLConfigHelper.configurePfxTrustOptions;
import java.io.IOException;
import java.io.InputStream;
@@ -54,6 +60,7 @@
import io.quarkus.grpc.runtime.config.GrpcClientConfiguration;
import io.quarkus.grpc.runtime.config.GrpcServerConfiguration;
import io.quarkus.grpc.runtime.config.SslClientConfig;
+import io.quarkus.grpc.runtime.config.TlsClientConfig;
import io.quarkus.grpc.runtime.stork.StorkGrpcChannel;
import io.quarkus.grpc.runtime.stork.StorkMeasuringGrpcInterceptor;
import io.quarkus.grpc.runtime.stork.VertxStorkMeasuringGrpcInterceptor;
@@ -250,7 +257,8 @@ public static Channel createChannel(String name, Set perClientIntercepto
return builder.build();
} else {
- HttpClientOptions options = new HttpClientOptions(); // TODO options
+ // Vert.x client
+ HttpClientOptions options = new HttpClientOptions();
options.setHttp2ClearTextUpgrade(false); // this fixes i30379
if (!plainText) {
@@ -258,20 +266,34 @@ public static Channel createChannel(String name, Set perClientIntercepto
options.setSsl(true);
options.setUseAlpn(true);
- if (config.ssl.trustStore.isPresent()) {
- Optional trustStorePath = config.ssl.trustStore;
- if (trustStorePath.isPresent()) {
- PemTrustOptions to = new PemTrustOptions();
- to.addCertValue(bufferFor(trustStorePath.get(), "trust store"));
- options.setTrustOptions(to);
- }
- Optional certificatePath = config.ssl.certificate;
- Optional keyPath = config.ssl.key;
- if (certificatePath.isPresent() && keyPath.isPresent()) {
- PemKeyCertOptions cko = new PemKeyCertOptions();
- cko.setCertValue(bufferFor(certificatePath.get(), "certificate"));
- cko.setKeyValue(bufferFor(keyPath.get(), "key"));
- options.setKeyCertOptions(cko);
+ TlsClientConfig tls = config.tls;
+ if (tls.enabled) {
+ options.setSsl(true).setTrustAll(tls.trustAll);
+
+ configurePemTrustOptions(options, tls.trustCertificatePem);
+ configureJksTrustOptions(options, tls.trustCertificateJks);
+ configurePfxTrustOptions(options, tls.trustCertificateP12);
+
+ configurePemKeyCertOptions(options, tls.keyCertificatePem);
+ configureJksKeyCertOptions(options, tls.keyCertificateJks);
+ configurePfxKeyCertOptions(options, tls.keyCertificateP12);
+ options.setVerifyHost(tls.verifyHostname);
+ } else {
+ if (config.ssl.trustStore.isPresent()) {
+ Optional trustStorePath = config.ssl.trustStore;
+ if (trustStorePath.isPresent()) {
+ PemTrustOptions to = new PemTrustOptions();
+ to.addCertValue(bufferFor(trustStorePath.get(), "trust store"));
+ options.setTrustOptions(to);
+ }
+ Optional certificatePath = config.ssl.certificate;
+ Optional keyPath = config.ssl.key;
+ if (certificatePath.isPresent() && keyPath.isPresent()) {
+ PemKeyCertOptions cko = new PemKeyCertOptions();
+ cko.setCertValue(bufferFor(certificatePath.get(), "certificate"));
+ cko.setKeyValue(bufferFor(keyPath.get(), "key"));
+ options.setKeyCertOptions(cko);
+ }
}
}
}
@@ -309,7 +331,7 @@ public static Channel createChannel(String name, Set perClientIntercepto
interceptors.addAll(interceptorContainer.getSortedPerServiceInterceptors(perClientInterceptors));
interceptors.addAll(interceptorContainer.getSortedGlobalInterceptors());
- LOGGER.info("Creating Vert.x gRPC channel ...");
+ LOGGER.debug("Creating Vert.x gRPC channel ...");
return new InternalGrpcChannel(client, channel, ClientInterceptors.intercept(channel, interceptors));
}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/SSLConfigHelper.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/SSLConfigHelper.java
new file mode 100644
index 00000000000000..c8e1f7e6cdbd74
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/SSLConfigHelper.java
@@ -0,0 +1,120 @@
+package io.quarkus.grpc.runtime.supports;
+
+import io.quarkus.grpc.runtime.config.TlsClientConfig;
+import io.vertx.core.net.JksOptions;
+import io.vertx.core.net.KeyCertOptions;
+import io.vertx.core.net.PemKeyCertOptions;
+import io.vertx.core.net.PemTrustOptions;
+import io.vertx.core.net.PfxOptions;
+import io.vertx.core.net.TCPSSLOptions;
+
+public class SSLConfigHelper {
+
+ public static void configurePemTrustOptions(TCPSSLOptions options,
+ TlsClientConfig.PemTrustCertConfiguration configuration) {
+ if ((configuration.certs.isPresent() && !configuration.certs.get().isEmpty())) {
+ ensureTrustOptionsNotSet(options);
+ options.setTrustOptions(toPemTrustOptions(configuration));
+ }
+ }
+
+ private static PemTrustOptions toPemTrustOptions(TlsClientConfig.PemTrustCertConfiguration configuration) {
+ PemTrustOptions pemTrustOptions = new PemTrustOptions();
+ if (configuration.certs.isPresent()) {
+ for (String cert : configuration.certs.get()) {
+ pemTrustOptions.addCertPath(cert);
+ }
+ }
+ return pemTrustOptions;
+ }
+
+ public static void configureJksTrustOptions(TCPSSLOptions options, TlsClientConfig.JksConfiguration configuration) {
+ if (configuration.path.isPresent()) {
+ ensureTrustOptionsNotSet(options);
+ options.setTrustOptions(toJksOptions(configuration));
+ }
+ }
+
+ private static JksOptions toJksOptions(TlsClientConfig.JksConfiguration configuration) {
+ JksOptions jksOptions = new JksOptions();
+ if (configuration.path.isPresent()) {
+ jksOptions.setPath(configuration.path.get());
+ }
+ if (configuration.password.isPresent()) {
+ jksOptions.setPassword(configuration.password.get());
+ }
+ return jksOptions;
+ }
+
+ public static void configurePfxTrustOptions(TCPSSLOptions options, TlsClientConfig.PfxConfiguration configuration) {
+ if (configuration.path.isPresent()) {
+ ensureTrustOptionsNotSet(options);
+ options.setTrustOptions(toPfxOptions(configuration));
+ }
+ }
+
+ private static PfxOptions toPfxOptions(TlsClientConfig.PfxConfiguration configuration) {
+ PfxOptions pfxOptions = new PfxOptions();
+ if (configuration.path.isPresent()) {
+ pfxOptions.setPath(configuration.path.get());
+ }
+ if (configuration.password.isPresent()) {
+ pfxOptions.setPassword(configuration.password.get());
+ }
+ return pfxOptions;
+ }
+
+ private static void ensureTrustOptionsNotSet(TCPSSLOptions options) {
+ if (options.getTrustOptions() != null) {
+ throw new IllegalArgumentException("Trust options have already been set");
+ }
+ }
+
+ public static void configurePemKeyCertOptions(TCPSSLOptions options,
+ TlsClientConfig.PemKeyCertConfiguration configuration) {
+ if (configuration.certs.isPresent() && !configuration.certs.get().isEmpty() && configuration.keys.isPresent()
+ && !configuration.keys.get().isEmpty()) {
+ ensureKeyCertOptionsNotSet(options);
+ options.setKeyCertOptions(toPemKeyCertOptions(configuration));
+ }
+ }
+
+ private static KeyCertOptions toPemKeyCertOptions(TlsClientConfig.PemKeyCertConfiguration configuration) {
+ PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions();
+ if (configuration.certs.isPresent()) {
+ for (String cert : configuration.certs.get()) {
+ pemKeyCertOptions.addCertPath(cert);
+ }
+ }
+ if (configuration.keys.isPresent()) {
+ for (String cert : configuration.keys.get()) {
+ pemKeyCertOptions.addKeyPath(cert);
+ }
+ }
+ return pemKeyCertOptions;
+ }
+
+ public static void configureJksKeyCertOptions(TCPSSLOptions options, TlsClientConfig.JksConfiguration configuration) {
+ if (configuration.path.isPresent()) {
+ ensureKeyCertOptionsNotSet(options);
+ options.setKeyCertOptions(toJksOptions(configuration));
+ }
+ }
+
+ public static void configurePfxKeyCertOptions(TCPSSLOptions options, TlsClientConfig.PfxConfiguration configuration) {
+ if (configuration.path.isPresent()) {
+ ensureKeyCertOptionsNotSet(options);
+ options.setKeyCertOptions(toPfxOptions(configuration));
+ }
+ }
+
+ private static void ensureKeyCertOptionsNotSet(TCPSSLOptions options) {
+ if (options.getKeyCertOptions() != null) {
+ throw new IllegalArgumentException("Key cert options have already been set");
+ }
+ }
+
+ private SSLConfigHelper() {
+ // Utility
+ }
+}
\ No newline at end of file
diff --git a/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversDisabledProcessor.java b/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversDisabledProcessor.java
index b2894c347c577f..6a9f0c05165418 100644
--- a/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversDisabledProcessor.java
+++ b/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversDisabledProcessor.java
@@ -37,9 +37,9 @@ public void disableHibernateEnversStaticInit(HibernateEnversRecorder recorder,
// TODO move this to runtime init once we implement in Hibernate ORM a way
// to remove entity types from the metamodel on runtime init
public void checkNoExplicitActiveTrue(HibernateEnversBuildTimeConfig buildTimeConfig) {
- for (var entry : buildTimeConfig.getAllPersistenceUnitConfigsAsMap().entrySet()) {
+ for (var entry : buildTimeConfig.persistenceUnits().entrySet()) {
var config = entry.getValue();
- if (config.active.isPresent() && config.active.get()) {
+ if (config.active().isPresent() && config.active().get()) {
var puName = entry.getKey();
String enabledPropertyKey = HibernateEnversBuildTimeConfig.extensionPropertyKey("enabled");
String activePropertyKey = HibernateEnversBuildTimeConfig.persistenceUnitPropertyKey(puName, "active");
diff --git a/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversEnabled.java b/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversEnabled.java
index 5451c0d2828e3c..035673f2b66eb2 100644
--- a/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversEnabled.java
+++ b/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversEnabled.java
@@ -18,7 +18,7 @@ public class HibernateEnversEnabled implements BooleanSupplier {
@Override
public boolean getAsBoolean() {
- return config.enabled;
+ return config.enabled();
}
}
diff --git a/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversProcessor.java b/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversProcessor.java
index a3b4d43cd46a43..d4f135cbee5c73 100644
--- a/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversProcessor.java
+++ b/extensions/hibernate-envers/deployment/src/main/java/io/quarkus/hibernate/envers/deployment/HibernateEnversProcessor.java
@@ -40,10 +40,10 @@ public void registerEnversReflections(BuildProducer re
.produce(ReflectiveClassBuildItem.builder("org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity")
.methods().build());
- for (HibernateEnversBuildTimeConfigPersistenceUnit pu : buildTimeConfig.getAllPersistenceUnitConfigsAsMap().values()) {
- pu.revisionListener.ifPresent(
+ for (HibernateEnversBuildTimeConfigPersistenceUnit pu : buildTimeConfig.persistenceUnits().values()) {
+ pu.revisionListener().ifPresent(
s -> reflectiveClass.produce(ReflectiveClassBuildItem.builder(s).methods().fields().build()));
- pu.auditStrategy.ifPresent(
+ pu.auditStrategy().ifPresent(
s -> reflectiveClass.produce(ReflectiveClassBuildItem.builder(s).methods().fields().build()));
}
}
diff --git a/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfig.java b/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfig.java
index 5c48492b5508e2..e88e1c94d6758b 100644
--- a/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfig.java
+++ b/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfig.java
@@ -1,17 +1,19 @@
package io.quarkus.hibernate.envers;
import java.util.Map;
-import java.util.TreeMap;
import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil;
import io.quarkus.runtime.annotations.ConfigDocMapKey;
-import io.quarkus.runtime.annotations.ConfigDocSection;
-import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithParentName;
+import io.smallrye.config.WithUnnamedKey;
+@ConfigMapping(prefix = "quarkus.hibernate-envers")
@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
-public class HibernateEnversBuildTimeConfig {
+public interface HibernateEnversBuildTimeConfig {
/**
* Whether Hibernate Envers is enabled during the build.
*
@@ -23,37 +25,22 @@ public class HibernateEnversBuildTimeConfig {
*
* @asciidoclet
*/
- @ConfigItem(defaultValue = "true")
- public boolean enabled;
+ @WithDefault("true")
+ boolean enabled();
/**
- * Configuration for the default persistence unit.
+ * Configuration for persistence units.
*/
- @ConfigItem(name = ConfigItem.PARENT)
- public HibernateEnversBuildTimeConfigPersistenceUnit defaultPersistenceUnit;
-
- /**
- * Configuration for additional named persistence units.
- */
- @ConfigDocSection
+ @WithParentName
+ @WithUnnamedKey(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME)
@ConfigDocMapKey("persistence-unit-name")
- @ConfigItem(name = ConfigItem.PARENT)
- public Map persistenceUnits;
-
- public Map getAllPersistenceUnitConfigsAsMap() {
- Map map = new TreeMap<>();
- if (defaultPersistenceUnit != null) {
- map.put(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME, defaultPersistenceUnit);
- }
- map.putAll(persistenceUnits);
- return map;
- }
+ Map persistenceUnits();
- public static String extensionPropertyKey(String radical) {
+ static String extensionPropertyKey(String radical) {
return "quarkus.hibernate-envers." + radical;
}
- public static String persistenceUnitPropertyKey(String persistenceUnitName, String radical) {
+ static String persistenceUnitPropertyKey(String persistenceUnitName, String radical) {
StringBuilder keyBuilder = new StringBuilder("quarkus.hibernate-envers.");
if (!PersistenceUnitUtil.isDefaultPersistenceUnit(persistenceUnitName)) {
keyBuilder.append("\"").append(persistenceUnitName).append("\".");
diff --git a/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfigPersistenceUnit.java b/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfigPersistenceUnit.java
index 8645657bd5037c..bdf85ee85615f2 100644
--- a/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfigPersistenceUnit.java
+++ b/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversBuildTimeConfigPersistenceUnit.java
@@ -2,11 +2,12 @@
import java.util.Optional;
+import io.quarkus.runtime.annotations.ConfigDocDefault;
import io.quarkus.runtime.annotations.ConfigGroup;
-import io.quarkus.runtime.annotations.ConfigItem;
+import io.smallrye.config.WithDefault;
@ConfigGroup
-public class HibernateEnversBuildTimeConfigPersistenceUnit {
+public interface HibernateEnversBuildTimeConfigPersistenceUnit {
/**
* Whether Hibernate Envers should be active for this persistence unit at runtime.
@@ -24,164 +25,163 @@ public class HibernateEnversBuildTimeConfigPersistenceUnit {
*
* @asciidoclet
*/
- @ConfigItem(defaultValueDocumentation = "'true' if Hibernate ORM is enabled; 'false' otherwise")
- public Optional active = Optional.empty();
+ @ConfigDocDefault("'true' if Hibernate ORM is enabled; 'false' otherwise")
+ Optional active();
/**
* Enable store_data_at_delete feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#STORE_DATA_AT_DELETE}.
*/
- @ConfigItem(defaultValue = "false")
- public boolean storeDataAtDelete;
+ @WithDefault("false")
+ boolean storeDataAtDelete();
/**
* Defines a suffix for historical data table. Defaults to {@literal _AUD}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#AUDIT_TABLE_SUFFIX}.
*/
- @ConfigItem(defaultValue = "_AUD")
- public Optional auditTableSuffix;
+ @WithDefault("_AUD")
+ Optional auditTableSuffix();
/**
* Defines a prefix for historical data table. Default is the empty string.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#AUDIT_TABLE_PREFIX}.
*/
- @ConfigItem(defaultValue = "")
- public Optional auditTablePrefix;
+ @WithDefault("")
+ Optional auditTablePrefix();
/**
* Revision field name. Defaults to {@literal REV}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#REVISION_FIELD_NAME}.
*/
- @ConfigItem(defaultValue = "REV")
- public Optional revisionFieldName;
+ @WithDefault("REV")
+ Optional revisionFieldName();
/**
* Revision type field name. Defaults to {@literal REVTYPE}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#REVISION_TYPE_FIELD_NAME}.
*/
- @ConfigItem(defaultValue = "REVTYPE")
- public Optional revisionTypeFieldName;
+ @WithDefault("REVTYPE")
+ Optional revisionTypeFieldName();
/**
* Enable the revision_on_collection_change feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#REVISION_ON_COLLECTION_CHANGE}.
*/
- @ConfigItem(defaultValue = "true")
- public boolean revisionOnCollectionChange;
+ @WithDefault("true")
+ boolean revisionOnCollectionChange();
/**
* Enable the do_not_audit_optimistic_locking_field feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#DO_NOT_AUDIT_OPTIMISTIC_LOCKING_FIELD}.
*/
- @ConfigItem(defaultValue = "true")
- public boolean doNotAuditOptimisticLockingField;
+ @WithDefault("true")
+ boolean doNotAuditOptimisticLockingField();
/**
* Defines the default schema of where audit tables are to be created.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#DEFAULT_SCHEMA}.
*/
- @ConfigItem(defaultValue = "")
- public Optional defaultSchema;
+ @WithDefault("")
+ Optional defaultSchema();
/**
* Defines the default catalog of where audit tables are to be created.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#DEFAULT_CATALOG}.
*/
- @ConfigItem(defaultValue = "")
- public Optional defaultCatalog;
+ @WithDefault("")
+ Optional defaultCatalog();
/**
* Enables the track_entities_changed_in_revision feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#TRACK_ENTITIES_CHANGED_IN_REVISION}.
*/
- @ConfigItem(defaultValue = "false")
- public boolean trackEntitiesChangedInRevision;
+ @WithDefault("false")
+ boolean trackEntitiesChangedInRevision();
/**
* Enables the use_revision_entity_with_native_id feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#USE_REVISION_ENTITY_WITH_NATIVE_ID}.
*/
- @ConfigItem(defaultValue = "true")
- public boolean useRevisionEntityWithNativeId;
+ @WithDefault("true")
+ boolean useRevisionEntityWithNativeId();
/**
* Enables the global_with_modified_flag feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#GLOBAL_WITH_MODIFIED_FLAG}.
*/
- @ConfigItem(defaultValue = "false")
- public boolean globalWithModifiedFlag;
+ @WithDefault("false")
+ boolean globalWithModifiedFlag();
/**
* Defines the suffix to be used for modified flag columns. Defaults to {@literal _MOD}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#MODIFIED_FLAG_SUFFIX}
*/
- @ConfigItem(defaultValue = "_MOD")
- public Optional modifiedFlagSuffix;
+ @WithDefault("_MOD")
+ Optional modifiedFlagSuffix();
/**
* Defines the fully qualified class name of a user defined revision listener.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#REVISION_LISTENER}.
*/
- @ConfigItem
- public Optional revisionListener;
+ Optional revisionListener();
/**
* Defines the fully qualified class name of the audit strategy to be used.
*
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#AUDIT_STRATEGY}.
*/
- @ConfigItem(defaultValue = "org.hibernate.envers.strategy.DefaultAuditStrategy")
- public Optional auditStrategy;
+ @WithDefault("org.hibernate.envers.strategy.DefaultAuditStrategy")
+ Optional auditStrategy();
/**
* Defines the property name for the audit entity's composite primary key. Defaults to {@literal originalId}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#ORIGINAL_ID_PROP_NAME}.
*/
- @ConfigItem(defaultValue = "originalId")
- public Optional originalIdPropName;
+ @WithDefault("originalId")
+ Optional originalIdPropName();
/**
* Defines the column name that holds the end revision number in audit entities. Defaults to {@literal REVEND}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#AUDIT_STRATEGY_VALIDITY_END_REV_FIELD_NAME}.
*/
- @ConfigItem(defaultValue = "REVEND")
- public Optional auditStrategyValidityEndRevFieldName;
+ @WithDefault("REVEND")
+ Optional auditStrategyValidityEndRevFieldName();
/**
* Enables the audit_strategy_validity_store_revend_timestamp feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#AUDIT_STRATEGY_VALIDITY_STORE_REVEND_TIMESTAMP}.
*/
- @ConfigItem(defaultValue = "false")
- public boolean auditStrategyValidityStoreRevendTimestamp;
+ @WithDefault("false")
+ boolean auditStrategyValidityStoreRevendTimestamp();
/**
* Defines the column name of the revision end timestamp in the audit tables. Defaults to {@literal REVEND_TSTMP}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_FIELD_NAME}.
*/
- @ConfigItem(defaultValue = "REVEND_TSTMP")
- public Optional auditStrategyValidityRevendTimestampFieldName;
+ @WithDefault("REVEND_TSTMP")
+ Optional auditStrategyValidityRevendTimestampFieldName();
/**
* Defines the name of the column used for storing collection ordinal values for embeddable elements.
* Defaults to {@literal SETORDINAL}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#EMBEDDABLE_SET_ORDINAL_FIELD_NAME}.
*/
- @ConfigItem(defaultValue = "SETORDINAL")
- public Optional embeddableSetOrdinalFieldName;
+ @WithDefault("SETORDINAL")
+ Optional embeddableSetOrdinalFieldName();
/**
* Enables the allow_identifier_reuse feature.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#ALLOW_IDENTIFIER_REUSE}.
*/
- @ConfigItem(defaultValue = "false")
- public boolean allowIdentifierReuse;
+ @WithDefault("false")
+ boolean allowIdentifierReuse();
/**
* Defines the naming strategy to be used for modified columns.
* Defaults to {@literal org.hibernate.envers.boot.internal.LegacyModifiedColumnNamingStrategy}.
* Maps to {@link org.hibernate.envers.configuration.EnversSettings#MODIFIED_COLUMN_NAMING_STRATEGY}.
*/
- @ConfigItem(defaultValue = "org.hibernate.envers.boot.internal.LegacyModifiedColumnNamingStrategy")
- public Optional modifiedColumnNamingStrategy;
+ @WithDefault("org.hibernate.envers.boot.internal.LegacyModifiedColumnNamingStrategy")
+ Optional modifiedColumnNamingStrategy();
}
diff --git a/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversRecorder.java b/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversRecorder.java
index 6bcc24e51088fa..3c32373213baed 100644
--- a/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversRecorder.java
+++ b/extensions/hibernate-envers/runtime/src/main/java/io/quarkus/hibernate/envers/HibernateEnversRecorder.java
@@ -31,48 +31,48 @@ private HibernateEnversIntegrationStaticInitListener(HibernateEnversBuildTimeCon
@Override
public void contributeBootProperties(BiConsumer propertyCollector) {
- var puConfig = buildTimeConfig.getAllPersistenceUnitConfigsAsMap().get(puName);
+ var puConfig = buildTimeConfig.persistenceUnits().get(puName);
if (puConfig == null) {
// Leave Envers unconfigured, but still activate it.
return;
}
- if (puConfig.active.isPresent() && !puConfig.active.get()) {
+ if (puConfig.active().isPresent() && !puConfig.active().get()) {
propertyCollector.accept(EnversService.INTEGRATION_ENABLED, "false");
// Do not process other properties: Hibernate Envers is inactive anyway.
return;
}
- addConfig(propertyCollector, EnversSettings.STORE_DATA_AT_DELETE, puConfig.storeDataAtDelete);
- addConfig(propertyCollector, EnversSettings.AUDIT_TABLE_SUFFIX, puConfig.auditTableSuffix);
- addConfig(propertyCollector, EnversSettings.AUDIT_TABLE_PREFIX, puConfig.auditTablePrefix);
- addConfig(propertyCollector, EnversSettings.REVISION_FIELD_NAME, puConfig.revisionFieldName);
- addConfig(propertyCollector, EnversSettings.REVISION_TYPE_FIELD_NAME, puConfig.revisionTypeFieldName);
+ addConfig(propertyCollector, EnversSettings.STORE_DATA_AT_DELETE, puConfig.storeDataAtDelete());
+ addConfig(propertyCollector, EnversSettings.AUDIT_TABLE_SUFFIX, puConfig.auditTableSuffix());
+ addConfig(propertyCollector, EnversSettings.AUDIT_TABLE_PREFIX, puConfig.auditTablePrefix());
+ addConfig(propertyCollector, EnversSettings.REVISION_FIELD_NAME, puConfig.revisionFieldName());
+ addConfig(propertyCollector, EnversSettings.REVISION_TYPE_FIELD_NAME, puConfig.revisionTypeFieldName());
addConfig(propertyCollector, EnversSettings.REVISION_ON_COLLECTION_CHANGE,
- puConfig.revisionOnCollectionChange);
+ puConfig.revisionOnCollectionChange());
addConfig(propertyCollector, EnversSettings.DO_NOT_AUDIT_OPTIMISTIC_LOCKING_FIELD,
- puConfig.doNotAuditOptimisticLockingField);
- addConfig(propertyCollector, EnversSettings.DEFAULT_SCHEMA, puConfig.defaultSchema);
- addConfig(propertyCollector, EnversSettings.DEFAULT_CATALOG, puConfig.defaultCatalog);
+ puConfig.doNotAuditOptimisticLockingField());
+ addConfig(propertyCollector, EnversSettings.DEFAULT_SCHEMA, puConfig.defaultSchema());
+ addConfig(propertyCollector, EnversSettings.DEFAULT_CATALOG, puConfig.defaultCatalog());
addConfig(propertyCollector, EnversSettings.TRACK_ENTITIES_CHANGED_IN_REVISION,
- puConfig.trackEntitiesChangedInRevision);
+ puConfig.trackEntitiesChangedInRevision());
addConfig(propertyCollector, EnversSettings.USE_REVISION_ENTITY_WITH_NATIVE_ID,
- puConfig.useRevisionEntityWithNativeId);
- addConfig(propertyCollector, EnversSettings.GLOBAL_WITH_MODIFIED_FLAG, puConfig.globalWithModifiedFlag);
- addConfig(propertyCollector, EnversSettings.MODIFIED_FLAG_SUFFIX, puConfig.modifiedFlagSuffix);
- addConfigIfPresent(propertyCollector, EnversSettings.REVISION_LISTENER, puConfig.revisionListener);
- addConfigIfPresent(propertyCollector, EnversSettings.AUDIT_STRATEGY, puConfig.auditStrategy);
- addConfigIfPresent(propertyCollector, EnversSettings.ORIGINAL_ID_PROP_NAME, puConfig.originalIdPropName);
+ puConfig.useRevisionEntityWithNativeId());
+ addConfig(propertyCollector, EnversSettings.GLOBAL_WITH_MODIFIED_FLAG, puConfig.globalWithModifiedFlag());
+ addConfig(propertyCollector, EnversSettings.MODIFIED_FLAG_SUFFIX, puConfig.modifiedFlagSuffix());
+ addConfigIfPresent(propertyCollector, EnversSettings.REVISION_LISTENER, puConfig.revisionListener());
+ addConfigIfPresent(propertyCollector, EnversSettings.AUDIT_STRATEGY, puConfig.auditStrategy());
+ addConfigIfPresent(propertyCollector, EnversSettings.ORIGINAL_ID_PROP_NAME, puConfig.originalIdPropName());
addConfigIfPresent(propertyCollector, EnversSettings.AUDIT_STRATEGY_VALIDITY_END_REV_FIELD_NAME,
- puConfig.auditStrategyValidityEndRevFieldName);
+ puConfig.auditStrategyValidityEndRevFieldName());
addConfig(propertyCollector, EnversSettings.AUDIT_STRATEGY_VALIDITY_STORE_REVEND_TIMESTAMP,
- puConfig.auditStrategyValidityStoreRevendTimestamp);
+ puConfig.auditStrategyValidityStoreRevendTimestamp());
addConfigIfPresent(propertyCollector, EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_FIELD_NAME,
- puConfig.auditStrategyValidityRevendTimestampFieldName);
+ puConfig.auditStrategyValidityRevendTimestampFieldName());
addConfigIfPresent(propertyCollector, EnversSettings.EMBEDDABLE_SET_ORDINAL_FIELD_NAME,
- puConfig.embeddableSetOrdinalFieldName);
- addConfig(propertyCollector, EnversSettings.ALLOW_IDENTIFIER_REUSE, puConfig.allowIdentifierReuse);
+ puConfig.embeddableSetOrdinalFieldName());
+ addConfig(propertyCollector, EnversSettings.ALLOW_IDENTIFIER_REUSE, puConfig.allowIdentifierReuse());
addConfigIfPresent(propertyCollector, EnversSettings.MODIFIED_COLUMN_NAMING_STRATEGY,
- puConfig.modifiedColumnNamingStrategy);
+ puConfig.modifiedColumnNamingStrategy());
}
public static void addConfig(BiConsumer propertyCollector, String configPath, T value) {
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateConfigUtil.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateConfigUtil.java
index 6e593f48a71de8..861a6f9d099ab5 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateConfigUtil.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateConfigUtil.java
@@ -17,17 +17,17 @@ public class HibernateConfigUtil {
public static Map getCacheConfigEntries(HibernateOrmConfigPersistenceUnit config) {
Map cacheRegionsConfigEntries = new HashMap<>();
- for (Map.Entry regionEntry : config.cache.entrySet()) {
+ for (Map.Entry regionEntry : config.cache().entrySet()) {
String regionName = regionEntry.getKey();
HibernateOrmConfigPersistenceUnitCache cacheConfig = regionEntry.getValue();
- if (cacheConfig.expiration.maxIdle.isPresent()) {
+ if (cacheConfig.expiration().maxIdle().isPresent()) {
cacheRegionsConfigEntries.put(getCacheConfigKey(regionName, EXPIRATION_MAX_IDLE),
- String.valueOf(cacheConfig.expiration.maxIdle.get().getSeconds()));
+ String.valueOf(cacheConfig.expiration().maxIdle().get().getSeconds()));
}
- if (cacheConfig.memory.objectCount.isPresent()) {
+ if (cacheConfig.memory().objectCount().isPresent()) {
cacheRegionsConfigEntries.put(getCacheConfigKey(regionName, MEMORY_OBJECT_COUNT),
- String.valueOf(cacheConfig.memory.objectCount.getAsLong()));
+ String.valueOf(cacheConfig.memory().objectCount().getAsLong()));
}
}
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java
index fc207f65444c32..7a17d32e38408e 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java
@@ -1,5 +1,6 @@
package io.quarkus.hibernate.orm.deployment;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@@ -11,6 +12,8 @@
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;
+import jakarta.persistence.AttributeConverter;
+import jakarta.transaction.TransactionManager;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
@@ -31,6 +34,7 @@
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator;
+import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.DotNames;
@@ -42,12 +46,16 @@
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.hibernate.orm.PersistenceUnit;
import io.quarkus.hibernate.orm.runtime.HibernateOrmRecorder;
import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig;
import io.quarkus.hibernate.orm.runtime.JPAConfig;
import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil;
+import io.quarkus.hibernate.orm.runtime.RequestScopedSessionHolder;
+import io.quarkus.hibernate.orm.runtime.RequestScopedStatelessSessionHolder;
import io.quarkus.hibernate.orm.runtime.TransactionSessions;
+import io.quarkus.hibernate.orm.runtime.cdi.QuarkusArcBeanContainer;
@BuildSteps(onlyIf = HibernateOrmEnabled.class)
public class HibernateOrmCdiProcessor {
@@ -182,7 +190,8 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder,
.addInjectionPoint(ClassType.create(DotName.createSimple(JPAConfig.class)))
.done());
- if (capabilities.isPresent(Capability.TRANSACTIONS)) {
+ if (capabilities.isPresent(Capability.TRANSACTIONS)
+ && capabilities.isMissing(Capability.HIBERNATE_REACTIVE)) {
// Do register a Session/EntityManager bean only if JTA is available
// Note that the Hibernate Reactive extension excludes JTA intentionally
syntheticBeanBuildItemBuildProducer
@@ -222,7 +231,8 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder,
.addInjectionPoint(ClassType.create(DotName.createSimple(JPAConfig.class)))
.done());
- if (capabilities.isPresent(Capability.TRANSACTIONS)) {
+ if (capabilities.isPresent(Capability.TRANSACTIONS)
+ && capabilities.isMissing(Capability.HIBERNATE_REACTIVE)) {
// Do register a Session/EntityManager bean only if JTA is available
// Note that the Hibernate Reactive extension excludes JTA intentionally
syntheticBeanBuildItemBuildProducer
@@ -245,6 +255,40 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder,
}
}
+ @BuildStep
+ void registerBeans(HibernateOrmConfig hibernateOrmConfig,
+ BuildProducer additionalBeans,
+ BuildProducer unremovableBeans,
+ Capabilities capabilities,
+ CombinedIndexBuildItem combinedIndex,
+ List descriptors,
+ JpaModelBuildItem jpaModel) {
+ if (!HibernateOrmProcessor.hasEntities(jpaModel)) {
+ return;
+ }
+
+ List> unremovableClasses = new ArrayList<>();
+ unremovableClasses.add(QuarkusArcBeanContainer.class);
+
+ if (capabilities.isMissing(Capability.HIBERNATE_REACTIVE)) {
+ // The following beans only make sense for Hibernate ORM, not for Hibernate Reactive
+
+ if (capabilities.isPresent(Capability.TRANSACTIONS)) {
+ unremovableClasses.add(TransactionManager.class);
+ unremovableClasses.add(TransactionSessions.class);
+ }
+ unremovableClasses.add(RequestScopedSessionHolder.class);
+ unremovableClasses.add(RequestScopedStatelessSessionHolder.class);
+ }
+ additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable()
+ .addBeanClasses(unremovableClasses.toArray(new Class>[unremovableClasses.size()]))
+ .build());
+
+ // Some user-injectable beans are retrieved programmatically and shouldn't be removed
+ unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(AttributeConverter.class));
+ unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(jpaModel.getPotentialCdiBeanClassNames()));
+ }
+
@BuildStep
void registerAnnotations(BuildProducer additionalBeans,
BuildProducer beanDefiningAnnotations) {
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java
index e2be11c863d715..32e60b297619c8 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java
@@ -9,12 +9,18 @@
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.ConfigRoot;
-import io.quarkus.runtime.annotations.ConvertWith;
-
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithConverter;
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithDefaults;
+import io.smallrye.config.WithName;
+import io.smallrye.config.WithParentName;
+import io.smallrye.config.WithUnnamedKey;
+
+@ConfigMapping(prefix = "quarkus.hibernate-orm")
@ConfigRoot
-public class HibernateOrmConfig {
+public interface HibernateOrmConfig {
/**
* Whether Hibernate ORM is enabled *during the build*.
@@ -25,49 +31,51 @@ public class HibernateOrmConfig {
*
* @asciidoclet
*/
- @ConfigItem(defaultValue = "true")
- public boolean enabled;
+ @WithDefault("true")
+ boolean enabled();
/**
* Database related configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigDatabase database;
-
- /**
- * Configuration for the default persistence unit.
- */
- @ConfigItem(name = ConfigItem.PARENT)
- public HibernateOrmConfigPersistenceUnit defaultPersistenceUnit;
+ HibernateOrmConfigDatabase database();
/**
- * Additional named persistence units.
+ * Configuration for persistence units.
*/
- @ConfigDocSection
+ @WithParentName
+ @WithUnnamedKey(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME)
+ @WithDefaults
@ConfigDocMapKey("persistence-unit-name")
- @ConfigItem(name = ConfigItem.PARENT)
- public Map persistenceUnits;
+ Map persistenceUnits();
+
+ default HibernateOrmConfigPersistenceUnit defaultPersistenceUnit() {
+ return persistenceUnits().get(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME);
+ }
+
+ default Map namedPersistenceUnits() {
+ Map map = new TreeMap<>();
+ map.putAll(persistenceUnits());
+ map.remove(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME);
+ return map;
+ }
/**
* Configuration for the {@code persistence.xml} handling.
*/
- @ConfigItem
- public HibernateOrmConfigPersistenceXml persistenceXml;
+ HibernateOrmConfigPersistenceXml persistenceXml();
/**
* Logging configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigLog log;
+ HibernateOrmConfigLog log();
/**
* Whether statistics collection is enabled. If 'metrics.enabled' is true, then the default here is
* considered true, otherwise the default is false.
*/
- @ConfigItem
- public Optional statistics;
+ Optional statistics();
/**
* Whether session metrics should be appended into the server log for each Hibernate session. This
@@ -75,73 +83,62 @@ public class HibernateOrmConfig {
* (which means both `statistics` and `log-session-metrics` need to be enabled for the session metrics
* to appear in the log).
*/
- @ConfigItem
- public Optional logSessionMetrics;
+ Optional logSessionMetrics();
/**
* Configuration related to metrics.
*/
- @ConfigItem
- public HibernateOrmConfigMetric metrics;
+ HibernateOrmConfigMetric metrics();
- public boolean isAnyNonPersistenceXmlPropertySet() {
+ default boolean isAnyNonPersistenceXmlPropertySet() {
// Do NOT include persistenceXml in here.
- return defaultPersistenceUnit.isAnyPropertySet() ||
- !persistenceUnits.isEmpty() ||
- log.isAnyPropertySet() ||
- statistics.isPresent() ||
- logSessionMetrics.isPresent() ||
- metrics.isAnyPropertySet();
- }
-
- public Map getAllPersistenceUnitConfigsAsMap() {
- Map map = new TreeMap<>();
- if (defaultPersistenceUnit != null) {
- map.put(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME, defaultPersistenceUnit);
- }
- map.putAll(persistenceUnits);
- return map;
+ return defaultPersistenceUnit().isAnyPropertySet() ||
+ !namedPersistenceUnits().isEmpty() ||
+ log().isAnyPropertySet() ||
+ statistics().isPresent() ||
+ logSessionMetrics().isPresent() ||
+ metrics().isAnyPropertySet();
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceXml {
+ interface HibernateOrmConfigPersistenceXml {
/**
* If {@code true}, Quarkus will ignore any {@code persistence.xml} file in the classpath
* and rely exclusively on the Quarkus configuration.
*/
- @ConfigItem
- public boolean ignore;
+ @WithDefault("false")
+ boolean ignore();
}
@ConfigGroup
- public static class HibernateOrmConfigLog {
+ interface HibernateOrmConfigLog {
/**
* Logs SQL bind parameter.
*
* Setting it to true is obviously not recommended in production.
*/
- @ConfigItem
@Deprecated
- public boolean bindParam;
+ @WithDefault("false")
+ boolean bindParam();
/**
* Logs SQL bind parameters.
*
* Setting it to true is obviously not recommended in production.
*/
- @ConfigItem
- public boolean bindParameters;
+ @WithDefault("false")
+ boolean bindParameters();
- public boolean isAnyPropertySet() {
- return bindParam || bindParameters;
+ default boolean isAnyPropertySet() {
+ return bindParam() || bindParameters();
}
}
@ConfigGroup
- public static class HibernateOrmConfigDatabase {
+ interface HibernateOrmConfigDatabase {
/**
* When set, attempts to exchange data with the database
* as the given version of Hibernate ORM would have,
@@ -165,22 +162,23 @@ public static class HibernateOrmConfigDatabase {
*
* @asciidoclet
*/
- @ConfigItem(name = "orm-compatibility.version", defaultValue = "latest")
- @ConvertWith(DatabaseOrmCompatibilityVersion.Converter.class)
- public DatabaseOrmCompatibilityVersion ormCompatibilityVersion;
+ @WithName("orm-compatibility.version")
+ @WithDefault("latest")
+ @WithConverter(DatabaseOrmCompatibilityVersion.Converter.class)
+ DatabaseOrmCompatibilityVersion ormCompatibilityVersion();
}
@ConfigGroup
- public static class HibernateOrmConfigMetric {
+ interface HibernateOrmConfigMetric {
/**
* Whether metrics are published if a metrics extension is enabled.
*/
- @ConfigItem
- public boolean enabled;
+ @WithDefault("false")
+ boolean enabled();
- public boolean isAnyPropertySet() {
- return enabled;
+ default boolean isAnyPropertySet() {
+ return enabled();
}
}
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java
index f87622b5957a0c..bdf64462f2836f 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java
@@ -2,7 +2,6 @@
import java.nio.charset.Charset;
import java.time.Duration;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -13,38 +12,38 @@
import org.hibernate.annotations.TimeZoneStorageType;
import org.hibernate.id.enhanced.StandardOptimizerDescriptor;
+import io.quarkus.runtime.annotations.ConfigDocDefault;
+import io.quarkus.runtime.annotations.ConfigDocIgnore;
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.ConvertWith;
import io.quarkus.runtime.configuration.TrimmedStringConverter;
+import io.smallrye.config.WithConverter;
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithName;
+import io.smallrye.config.WithParentName;
@ConfigGroup
-public class HibernateOrmConfigPersistenceUnit {
+public interface HibernateOrmConfigPersistenceUnit {
/**
* The name of the datasource which this persistence unit uses.
*
* If undefined, it will use the default datasource.
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional datasource;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional datasource();
/**
* The packages in which the entities affected to this persistence unit are located.
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional> packages;
+ Optional> packages();
/**
* Dialect related configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigPersistenceUnitDialect dialect;
+ HibernateOrmConfigPersistenceUnitDialect dialect();
// @formatter:off
/**
@@ -83,9 +82,8 @@ public class HibernateOrmConfigPersistenceUnit {
* @asciidoclet
*/
// @formatter:on
- @ConfigItem(defaultValueDocumentation = "import.sql in DEV, TEST ; no-file otherwise")
- @ConvertWith(TrimmedStringConverter.class)
- public Optional> sqlLoadScript;
+ @ConfigDocDefault("import.sql in DEV, TEST ; no-file otherwise")
+ Optional> sqlLoadScript();
/**
* The size of the batches used when loading entities and collections.
@@ -95,9 +93,9 @@ public class HibernateOrmConfigPersistenceUnit {
* @deprecated {@link #fetch} should be used to configure fetching properties.
* @asciidoclet
*/
- @ConfigItem(defaultValueDocumentation = "16")
+ @ConfigDocDefault("16")
@Deprecated
- public OptionalInt batchFetchSize;
+ OptionalInt batchFetchSize();
/**
* The maximum depth of outer join fetch tree for single-ended associations (one-to-one, many-to-one).
@@ -107,27 +105,24 @@ public class HibernateOrmConfigPersistenceUnit {
* @deprecated {@link #fetch} should be used to configure fetching properties.
* @asciidoclet
*/
- @ConfigItem
@Deprecated
- public OptionalInt maxFetchDepth;
+ OptionalInt maxFetchDepth();
/**
* Pluggable strategy contract for applying physical naming rules for database object names.
*
* Class name of the Hibernate PhysicalNamingStrategy implementation
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional physicalNamingStrategy;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional physicalNamingStrategy();
/**
* Pluggable strategy for applying implicit naming rules when an explicit name is not given.
*
* Class name of the Hibernate ImplicitNamingStrategy implementation
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional implicitNamingStrategy;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional implicitNamingStrategy();
/**
* Class name of a custom
@@ -145,9 +140,8 @@ public class HibernateOrmConfigPersistenceUnit {
*
* @asciidoclet
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional metadataBuilderContributor;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional metadataBuilderContributor();
/**
* XML files to configure the entity mapping, e.g. {@code META-INF/my-orm.xml}.
@@ -155,64 +149,55 @@ public class HibernateOrmConfigPersistenceUnit {
* Defaults to `META-INF/orm.xml` if it exists.
* Pass `no-file` to force Hibernate ORM to ignore `META-INF/orm.xml`.
*/
- @ConfigItem(defaultValueDocumentation = "META-INF/orm.xml if it exists; no-file otherwise")
- @ConvertWith(TrimmedStringConverter.class)
- public Optional> mappingFiles;
+ @ConfigDocDefault("META-INF/orm.xml if it exists; no-file otherwise")
+ Optional> mappingFiles();
/**
* Mapping configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigPersistenceUnitMapping mapping;
+ HibernateOrmConfigPersistenceUnitMapping mapping();
/**
* Query related configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigPersistenceUnitQuery query;
+ HibernateOrmConfigPersistenceUnitQuery query();
/**
* Database related configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigPersistenceUnitDatabase database;
+ HibernateOrmConfigPersistenceUnitDatabase database();
/**
* JDBC related configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigPersistenceUnitJdbc jdbc;
+ HibernateOrmConfigPersistenceUnitJdbc jdbc();
/**
* Fetching logic configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigPersistenceUnitFetch fetch;
+ HibernateOrmConfigPersistenceUnitFetch fetch();
/**
* Caching configuration
*/
- @ConfigItem
@ConfigDocSection
- public Map cache;
+ Map cache();
/**
* Discriminator related configuration.
*/
- @ConfigItem
@ConfigDocSection
- public HibernateOrmConfigPersistenceUnitDiscriminator discriminator;
+ HibernateOrmConfigPersistenceUnitDiscriminator discriminator();
/**
* Config related to identifier quoting.
*/
- @ConfigItem(defaultValue = "none")
- public HibernateOrmConfigPersistenceUnitQuoteIdentifiers quoteIdentifiers;
+ HibernateOrmConfigPersistenceUnitQuoteIdentifiers quoteIdentifiers();
/**
* The default in Quarkus is for 2nd level caching to be enabled,
@@ -222,14 +207,13 @@ public class HibernateOrmConfigPersistenceUnit {
*
* Set this to false to disable all 2nd level caches.
*/
- @ConfigItem(defaultValue = "true")
- public boolean secondLevelCachingEnabled;
+ @WithDefault("true")
+ boolean secondLevelCachingEnabled();
/**
* Bean Validation configuration.
*/
- @ConfigItem
- public HibernateOrmConfigPersistenceValidation validation;
+ HibernateOrmConfigPersistenceValidation validation();
/**
* Defines the method for multi-tenancy (DATABASE, NONE, SCHEMA). The complete list of allowed values is available in the
@@ -239,56 +223,54 @@ public class HibernateOrmConfigPersistenceUnit {
*
* @asciidoclet
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional multitenant;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional multitenant();
/**
* Defines the name of the datasource to use in case of SCHEMA approach. The datasource of the persistence unit will be used
* if not set.
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional multitenantSchemaDatasource;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional multitenantSchemaDatasource();
/**
* If hibernate is not auto generating the schema, and Quarkus is running in development mode
* then Quarkus will attempt to validate the database after startup and print a log message if
* there are any problems.
*/
- @ConfigItem(defaultValue = "true")
- public boolean validateInDevMode;
+ @WithDefault("true")
+ boolean validateInDevMode();
- @ConfigItem(generateDocumentation = false)
+ @ConfigDocIgnore
@ConfigDocMapKey("full-property-key")
- public Map unsupportedProperties = new HashMap<>();
-
- public boolean isAnyPropertySet() {
- return datasource.isPresent() ||
- packages.isPresent() ||
- dialect.isAnyPropertySet() ||
- sqlLoadScript.isPresent() ||
- batchFetchSize.isPresent() ||
- maxFetchDepth.isPresent() ||
- physicalNamingStrategy.isPresent() ||
- implicitNamingStrategy.isPresent() ||
- metadataBuilderContributor.isPresent() ||
- mapping.isAnyPropertySet() ||
- query.isAnyPropertySet() ||
- database.isAnyPropertySet() ||
- jdbc.isAnyPropertySet() ||
- !cache.isEmpty() ||
- !secondLevelCachingEnabled ||
- multitenant.isPresent() ||
- multitenantSchemaDatasource.isPresent() ||
- fetch.isAnyPropertySet() ||
- discriminator.isAnyPropertySet() ||
- quoteIdentifiers.isAnyPropertySet() ||
- !unsupportedProperties.isEmpty();
+ Map unsupportedProperties();
+
+ default boolean isAnyPropertySet() {
+ return datasource().isPresent() ||
+ packages().isPresent() ||
+ dialect().isAnyPropertySet() ||
+ sqlLoadScript().isPresent() ||
+ batchFetchSize().isPresent() ||
+ maxFetchDepth().isPresent() ||
+ physicalNamingStrategy().isPresent() ||
+ implicitNamingStrategy().isPresent() ||
+ metadataBuilderContributor().isPresent() ||
+ mapping().isAnyPropertySet() ||
+ query().isAnyPropertySet() ||
+ database().isAnyPropertySet() ||
+ jdbc().isAnyPropertySet() ||
+ !cache().isEmpty() ||
+ !secondLevelCachingEnabled() ||
+ multitenant().isPresent() ||
+ multitenantSchemaDatasource().isPresent() ||
+ fetch().isAnyPropertySet() ||
+ discriminator().isAnyPropertySet() ||
+ quoteIdentifiers().isAnyPropertySet() ||
+ !unsupportedProperties().isEmpty();
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitDialect {
+ interface HibernateOrmConfigPersistenceUnitDialect {
/**
* Class name of the Hibernate ORM dialect.
@@ -313,9 +295,10 @@ public static class HibernateOrmConfigPersistenceUnitDialect {
*
* @asciidoclet
*/
- @ConfigItem(name = ConfigItem.PARENT, defaultValueDocumentation = "selected automatically for most popular databases")
- @ConvertWith(TrimmedStringConverter.class)
- public Optional dialect;
+ @WithParentName
+ @ConfigDocDefault("selected automatically for most popular databases")
+ @WithConverter(TrimmedStringConverter.class)
+ Optional dialect();
/**
* The storage engine to use when the dialect supports multiple storage engines.
@@ -324,12 +307,11 @@ public static class HibernateOrmConfigPersistenceUnitDialect {
*
* @asciidoclet
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional storageEngine;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional storageEngine();
- public boolean isAnyPropertySet() {
- return dialect.isPresent() || storageEngine.isPresent();
+ default boolean isAnyPropertySet() {
+ return dialect().isPresent() || storageEngine().isPresent();
}
}
@@ -337,21 +319,19 @@ public boolean isAnyPropertySet() {
* Mapping-related configuration.
*/
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitMapping {
+ interface HibernateOrmConfigPersistenceUnitMapping {
/**
* Timezone configuration.
*/
- @ConfigItem
- public Timezone timezone;
+ Timezone timezone();
/**
* Optimizer configuration.
*/
- @ConfigItem
- public Id id;
+ Id id();
@ConfigGroup
- public static class Timezone {
+ interface Timezone {
/**
* How to store timezones in the database by default
* for properties of type `OffsetDateTime` and `ZonedDateTime`.
@@ -391,20 +371,20 @@ public static class Timezone {
*
* @asciidoclet
*/
- @ConfigItem(name = "default-storage", defaultValueDocumentation = "default")
- public Optional timeZoneDefaultStorage;
+ @WithName("default-storage")
+ @ConfigDocDefault("default")
+ Optional timeZoneDefaultStorage();
}
@ConfigGroup
- public static class Id {
+ interface Id {
/**
* Optimizer configuration.
*/
- @ConfigItem
- public Optimizer optimizer;
+ Optimizer optimizer();
@ConfigGroup
- public static class Optimizer {
+ interface Optimizer {
/**
* The optimizer to apply to identifier generators
* whose optimizer is not configured explicitly.
@@ -418,24 +398,25 @@ public static class Optimizer {
*
* @asciidoclet
*/
- @ConfigItem(name = "default", defaultValueDocumentation = "pooled-lo")
+ @WithName("default")
+ @ConfigDocDefault("pooled-lo")
// Note this needs to be a build-time property due to
// org.hibernate.boot.internal.InFlightMetadataCollectorImpl.handleIdentifierValueBinding
// which may call (indirectly) org.hibernate.id.enhanced.SequenceStructure.buildSequence
// whose output depends on org.hibernate.id.enhanced.SequenceStructure.applyIncrementSizeToSourceValues
// which is determined by the optimizer.
- public Optional idOptimizerDefault;
+ Optional idOptimizerDefault();
}
}
- public boolean isAnyPropertySet() {
- return timezone.timeZoneDefaultStorage.isPresent()
- || id.optimizer.idOptimizerDefault.isPresent();
+ default boolean isAnyPropertySet() {
+ return timezone().timeZoneDefaultStorage().isPresent()
+ || id().optimizer().idOptimizerDefault().isPresent();
}
}
- public enum IdOptimizerType {
+ enum IdOptimizerType {
/**
* Assumes the value retrieved from the table/sequence is the lower end of the pool.
*
@@ -489,11 +470,11 @@ public enum IdOptimizerType {
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitQuery {
+ interface HibernateOrmConfigPersistenceUnitQuery {
- private static final int DEFAULT_QUERY_PLAN_CACHE_MAX_SIZE = 2048;
+ int DEFAULT_QUERY_PLAN_CACHE_MAX_SIZE = 2048;
- public enum NullOrdering {
+ enum NullOrdering {
NONE,
FIRST,
LAST
@@ -503,8 +484,8 @@ public enum NullOrdering {
* The maximum size of the query plan cache.
* see #{@value org.hibernate.cfg.AvailableSettings#QUERY_PLAN_CACHE_MAX_SIZE}
*/
- @ConfigItem(defaultValue = "2048")
- public int queryPlanCacheMaxSize;
+ @WithDefault("2048")
+ int queryPlanCacheMaxSize();
/**
* Default precedence of null values in `ORDER BY` clauses.
@@ -513,114 +494,107 @@ public enum NullOrdering {
*
* @asciidoclet
*/
- @ConfigItem(defaultValue = "none")
- public NullOrdering defaultNullOrdering;
+ @WithDefault("none")
+ NullOrdering defaultNullOrdering();
/**
* Enables IN clause parameter padding which improves statement caching.
*/
- @ConfigItem(defaultValue = "true")
- public boolean inClauseParameterPadding;
+ @WithDefault("true")
+ boolean inClauseParameterPadding();
- public boolean isAnyPropertySet() {
- return queryPlanCacheMaxSize != DEFAULT_QUERY_PLAN_CACHE_MAX_SIZE
- || defaultNullOrdering != NullOrdering.NONE
- || !inClauseParameterPadding;
+ default boolean isAnyPropertySet() {
+ return queryPlanCacheMaxSize() != DEFAULT_QUERY_PLAN_CACHE_MAX_SIZE
+ || defaultNullOrdering() != NullOrdering.NONE
+ || !inClauseParameterPadding();
}
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitDatabase {
+ interface HibernateOrmConfigPersistenceUnitDatabase {
- private static final String DEFAULT_CHARSET = "UTF-8";
+ String DEFAULT_CHARSET = "UTF-8";
/**
* The charset of the database.
*
* Used for DDL generation and also for the SQL import scripts.
*/
- @ConfigItem(defaultValue = DEFAULT_CHARSET)
- public Charset charset;
+ @WithDefault(DEFAULT_CHARSET)
+ Charset charset();
/**
* Whether Hibernate should quote all identifiers.
*
* @deprecated {@link #quoteIdentifiers} should be used to configure quoting strategy.
*/
- @ConfigItem
@Deprecated
- public boolean globallyQuotedIdentifiers;
+ @WithDefault("false")
+ boolean globallyQuotedIdentifiers();
- public boolean isAnyPropertySet() {
- return !DEFAULT_CHARSET.equals(charset.name())
- || globallyQuotedIdentifiers;
+ default boolean isAnyPropertySet() {
+ return !DEFAULT_CHARSET.equals(charset().name())
+ || globallyQuotedIdentifiers();
}
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitJdbc {
+ interface HibernateOrmConfigPersistenceUnitJdbc {
/**
* The time zone pushed to the JDBC driver.
*
* See `quarkus.hibernate-orm.mapping.timezone.default-storage`.
*/
- @ConfigItem
- @ConvertWith(TrimmedStringConverter.class)
- public Optional timezone;
+ @WithConverter(TrimmedStringConverter.class)
+ Optional timezone();
/**
* How many rows are fetched at a time by the JDBC driver.
*/
- @ConfigItem
- public OptionalInt statementFetchSize;
+ OptionalInt statementFetchSize();
/**
* The number of updates (inserts, updates and deletes) that are sent by the JDBC driver at one time for execution.
*/
- @ConfigItem
- public OptionalInt statementBatchSize;
+ OptionalInt statementBatchSize();
- public boolean isAnyPropertySet() {
- return timezone.isPresent() || statementFetchSize.isPresent() || statementBatchSize.isPresent();
+ default boolean isAnyPropertySet() {
+ return timezone().isPresent() || statementFetchSize().isPresent() || statementBatchSize().isPresent();
}
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitCache {
+ interface HibernateOrmConfigPersistenceUnitCache {
/**
* The cache expiration configuration.
*/
- @ConfigItem
- public HibernateOrmConfigPersistenceUnitCacheExpiration expiration;
+ HibernateOrmConfigPersistenceUnitCacheExpiration expiration();
/**
* The cache memory storage configuration.
*/
- @ConfigItem
- public HibernateOrmConfigPersistenceUnitCacheMemory memory;
+ HibernateOrmConfigPersistenceUnitCacheMemory memory();
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitCacheExpiration {
+ interface HibernateOrmConfigPersistenceUnitCacheExpiration {
/**
* The maximum time before an object of the cache is considered expired.
*/
- @ConfigItem
- public Optional maxIdle;
+ Optional maxIdle();
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitCacheMemory {
+ interface HibernateOrmConfigPersistenceUnitCacheMemory {
/**
* The maximum number of objects kept in memory in the cache.
*/
- @ConfigItem
- public OptionalLong objectCount;
+ OptionalLong objectCount();
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitFetch {
+ interface HibernateOrmConfigPersistenceUnitFetch {
/**
* The size of the batches used when loading entities and collections.
*
@@ -628,8 +602,8 @@ public static class HibernateOrmConfigPersistenceUnitFetch {
*
* @asciidoclet
*/
- @ConfigItem(defaultValueDocumentation = "16")
- public OptionalInt batchSize;
+ @ConfigDocDefault("16")
+ OptionalInt batchSize();
/**
* The maximum depth of outer join fetch tree for single-ended associations (one-to-one, many-to-one).
@@ -638,17 +612,16 @@ public static class HibernateOrmConfigPersistenceUnitFetch {
*
* @asciidoclet
*/
- @ConfigItem
- public OptionalInt maxDepth;
+ OptionalInt maxDepth();
- public boolean isAnyPropertySet() {
- return batchSize.isPresent() || maxDepth.isPresent();
+ default boolean isAnyPropertySet() {
+ return batchSize().isPresent() || maxDepth().isPresent();
}
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitQuoteIdentifiers {
+ interface HibernateOrmConfigPersistenceUnitQuoteIdentifiers {
/**
* Identifiers can be quoted using one of the available strategies.
@@ -660,11 +633,11 @@ public static class HibernateOrmConfigPersistenceUnitQuoteIdentifiers {
* only
* identifiers deemed SQL keywords by the Hibernate ORM dialect.
*/
- @ConfigItem(defaultValue = "none")
- public IdentifierQuotingStrategy strategy;
+ @WithDefault("none")
+ IdentifierQuotingStrategy strategy();
- public boolean isAnyPropertySet() {
- return strategy != IdentifierQuotingStrategy.NONE;
+ default boolean isAnyPropertySet() {
+ return strategy() != IdentifierQuotingStrategy.NONE;
}
}
@@ -674,21 +647,21 @@ public boolean isAnyPropertySet() {
* Separated in a group configuration, in case it is necessary to add the another existing hibernate discriminator property.
*/
@ConfigGroup
- public static class HibernateOrmConfigPersistenceUnitDiscriminator {
+ interface HibernateOrmConfigPersistenceUnitDiscriminator {
/**
* Existing applications rely (implicitly or explicitly) on Hibernate ignoring any DiscriminatorColumn declarations on
* joined inheritance hierarchies. This setting allows these applications to maintain the legacy behavior of
* DiscriminatorColumn annotations being ignored when paired with joined inheritance.
*/
- @ConfigItem
- public boolean ignoreExplicitForJoined;
+ @WithDefault("false")
+ boolean ignoreExplicitForJoined();
- public boolean isAnyPropertySet() {
- return ignoreExplicitForJoined;
+ default boolean isAnyPropertySet() {
+ return ignoreExplicitForJoined();
}
}
- public enum IdentifierQuotingStrategy {
+ enum IdentifierQuotingStrategy {
NONE,
ALL,
ALL_EXCEPT_COLUMN_DEFINITIONS,
@@ -696,13 +669,13 @@ public enum IdentifierQuotingStrategy {
}
@ConfigGroup
- public static class HibernateOrmConfigPersistenceValidation {
+ interface HibernateOrmConfigPersistenceValidation {
/**
* Enables the Bean Validation integration.
*/
- @ConfigItem(defaultValue = "true")
- public boolean enabled;
+ @WithDefault("true")
+ boolean enabled();
}
}
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmEnabled.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmEnabled.java
index b5ed9ddb3183cb..7e9d0c63c43046 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmEnabled.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmEnabled.java
@@ -16,7 +16,7 @@ public class HibernateOrmEnabled implements BooleanSupplier {
@Override
public boolean getAsBoolean() {
- return config.enabled;
+ return config.enabled();
}
}
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java
index 2ad2bb9e78ac7d..c93d00b8ce5a79 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java
@@ -36,11 +36,9 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Default;
-import jakarta.persistence.AttributeConverter;
import jakarta.persistence.SharedCacheMode;
import jakarta.persistence.ValidationMode;
import jakarta.persistence.spi.PersistenceUnitTransactionType;
-import jakarta.transaction.TransactionManager;
import jakarta.xml.bind.JAXBElement;
import org.hibernate.boot.archive.scan.spi.ClassDescriptor;
@@ -66,7 +64,6 @@
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.agroal.spi.JdbcDataSourceSchemaReadyBuildItem;
-import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.BeanContainerListenerBuildItem;
import io.quarkus.arc.deployment.RecorderBeanInitializedBuildItem;
@@ -120,15 +117,11 @@
import io.quarkus.hibernate.orm.runtime.HibernateOrmRecorder;
import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig;
import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil;
-import io.quarkus.hibernate.orm.runtime.RequestScopedSessionHolder;
-import io.quarkus.hibernate.orm.runtime.RequestScopedStatelessSessionHolder;
-import io.quarkus.hibernate.orm.runtime.TransactionSessions;
import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDefinition;
import io.quarkus.hibernate.orm.runtime.boot.scan.QuarkusScanner;
import io.quarkus.hibernate.orm.runtime.boot.xml.JAXBElementSubstitution;
import io.quarkus.hibernate.orm.runtime.boot.xml.QNameSubstitution;
import io.quarkus.hibernate.orm.runtime.boot.xml.RecordableXmlMapping;
-import io.quarkus.hibernate.orm.runtime.cdi.QuarkusArcBeanContainer;
import io.quarkus.hibernate.orm.runtime.config.DialectVersions;
import io.quarkus.hibernate.orm.runtime.dev.HibernateOrmDevIntegrator;
import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor;
@@ -202,10 +195,10 @@ void checkTransactionsSupport(Capabilities capabilities, BuildProducer additionalApplicationArchiveMarkers) {
- for (HibernateOrmConfigPersistenceUnit persistenceUnit : hibernateOrmConfig.getAllPersistenceUnitConfigsAsMap()
+ for (HibernateOrmConfigPersistenceUnit persistenceUnit : hibernateOrmConfig.persistenceUnits()
.values()) {
- if (persistenceUnit.packages.isPresent()) {
- for (String pakkage : persistenceUnit.packages.get()) {
+ if (persistenceUnit.packages().isPresent()) {
+ for (String pakkage : persistenceUnit.packages().get()) {
additionalApplicationArchiveMarkers
.produce(new AdditionalApplicationArchiveMarkerBuildItem(pakkage.replace('.', '/')));
}
@@ -217,8 +210,8 @@ void includeArchivesHostingEntityPackagesInIndex(HibernateOrmConfig hibernateOrm
@Consume(ServiceStartBuildItem.class)
@BuildStep(onlyIf = IsDevelopment.class)
void warnOfSchemaProblems(HibernateOrmConfig config, HibernateOrmRecorder recorder) {
- for (var e : config.getAllPersistenceUnitConfigsAsMap().entrySet()) {
- if (e.getValue().validateInDevMode) {
+ for (var e : config.persistenceUnits().entrySet()) {
+ if (e.getValue().validateInDevMode()) {
recorder.doValidation(e.getKey());
}
}
@@ -323,7 +316,7 @@ public void configurationDescriptorBuilding(
getMultiTenancyStrategy(
Optional.ofNullable(persistenceXmlDescriptorBuildItem.getDescriptor()
.getProperties().getProperty("hibernate.multiTenancy"))), //FIXME this property is meaningless in Hibernate ORM 6
- hibernateOrmConfig.database.ormCompatibilityVersion, Collections.emptyMap()),
+ hibernateOrmConfig.database().ormCompatibilityVersion(), Collections.emptyMap()),
null,
jpaModel.getXmlMappings(persistenceXmlDescriptorBuildItem.getDescriptor().getName()),
false, true, capabilities));
@@ -377,13 +370,13 @@ public void contributePersistenceXmlToJpaModel(
public void contributeQuarkusConfigToJpaModel(
BuildProducer jpaModelPuContributions,
HibernateOrmConfig hibernateOrmConfig) {
- for (Entry entry : hibernateOrmConfig.getAllPersistenceUnitConfigsAsMap()
+ for (Entry entry : hibernateOrmConfig.persistenceUnits()
.entrySet()) {
String name = entry.getKey();
HibernateOrmConfigPersistenceUnit config = entry.getValue();
jpaModelPuContributions.produce(new JpaModelPersistenceUnitContributionBuildItem(
name, null, Collections.emptySet(),
- config.mappingFiles.orElse(Collections.emptySet())));
+ config.mappingFiles().orElse(Collections.emptySet())));
}
}
@@ -592,36 +585,6 @@ void handleNativeImageImportSql(BuildProducer reso
}
}
- @BuildStep
- void registerBeans(HibernateOrmConfig hibernateOrmConfig,
- BuildProducer additionalBeans,
- BuildProducer unremovableBeans,
- Capabilities capabilities,
- CombinedIndexBuildItem combinedIndex,
- List descriptors,
- JpaModelBuildItem jpaModel) {
- if (!hasEntities(jpaModel)) {
- return;
- }
-
- List> unremovableClasses = new ArrayList<>();
- if (capabilities.isPresent(Capability.TRANSACTIONS)) {
- unremovableClasses.add(TransactionManager.class);
- unremovableClasses.add(TransactionSessions.class);
- }
- unremovableClasses.add(RequestScopedSessionHolder.class);
- unremovableClasses.add(RequestScopedStatelessSessionHolder.class);
- unremovableClasses.add(QuarkusArcBeanContainer.class);
-
- additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable()
- .addBeanClasses(unremovableClasses.toArray(new Class>[unremovableClasses.size()]))
- .build());
-
- // Some user-injectable beans are retrieved programmatically and shouldn't be removed
- unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(AttributeConverter.class));
- unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(jpaModel.getPotentialCdiBeanClassNames()));
- }
-
@Consume(InterceptedStaticMethodsTransformersRegisteredBuildItem.class)
@BuildStep
@SuppressWarnings("deprecation")
@@ -743,7 +706,7 @@ public void multitenancy(HibernateOrmRecorder recorder,
@BuildStep
public void produceLoggingCategories(HibernateOrmConfig hibernateOrmConfig,
BuildProducer categories) {
- if (hibernateOrmConfig.log.bindParam || hibernateOrmConfig.log.bindParameters) {
+ if (hibernateOrmConfig.log().bindParam() || hibernateOrmConfig.log().bindParameters()) {
categories.produce(new LogCategoryBuildItem("org.hibernate.orm.jdbc.bind", Level.TRACE, true));
}
}
@@ -801,7 +764,7 @@ private static List getSqlLoadScript(Optional> sqlLoadScrip
}
}
- private boolean hasEntities(JpaModelBuildItem jpaModel) {
+ static boolean hasEntities(JpaModelBuildItem jpaModel) {
return !jpaModel.getEntityClassNames().isEmpty();
}
@@ -820,7 +783,7 @@ private void handleHibernateORMWithNoPersistenceXml(
BuildProducer persistenceUnitDescriptors,
List dbKindMetadataBuildItems) {
if (!descriptors.isEmpty()) {
- if (hibernateOrmConfig.isAnyNonPersistenceXmlPropertySet() || !hibernateOrmConfig.persistenceUnits.isEmpty()) {
+ if (hibernateOrmConfig.isAnyNonPersistenceXmlPropertySet()) {
throw new ConfigurationException(
"A legacy persistence.xml file is present in the classpath, but Hibernate ORM is also configured through the Quarkus config file.\n"
+ "Legacy persistence.xml files and Quarkus configuration cannot be used at the same time.\n"
@@ -848,8 +811,8 @@ private void handleHibernateORMWithNoPersistenceXml(
.filter(i -> i.isDefault())
.findFirst();
boolean enableDefaultPersistenceUnit = (defaultJdbcDataSource.isPresent()
- && hibernateOrmConfig.persistenceUnits.isEmpty())
- || hibernateOrmConfig.defaultPersistenceUnit.isAnyPropertySet();
+ && hibernateOrmConfig.namedPersistenceUnits().isEmpty())
+ || hibernateOrmConfig.defaultPersistenceUnit().isAnyPropertySet();
Map> modelClassesAndPackagesPerPersistencesUnits = getModelClassesAndPackagesPerPersistenceUnits(
hibernateOrmConfig, jpaModel, index.getIndex(), enableDefaultPersistenceUnit);
@@ -861,15 +824,15 @@ private void handleHibernateORMWithNoPersistenceXml(
if (enableDefaultPersistenceUnit) {
producePersistenceUnitDescriptorFromConfig(
hibernateOrmConfig, PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME,
- hibernateOrmConfig.defaultPersistenceUnit,
+ hibernateOrmConfig.defaultPersistenceUnit(),
modelClassesAndPackagesForDefaultPersistenceUnit,
jpaModel.getXmlMappings(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME),
jdbcDataSources, applicationArchivesBuildItem, launchMode, capabilities,
systemProperties, nativeImageResources, hotDeploymentWatchedFiles, persistenceUnitDescriptors,
storageEngineCollector, dbKindMetadataBuildItems);
} else if (!modelClassesAndPackagesForDefaultPersistenceUnit.isEmpty()
- && (!hibernateOrmConfig.defaultPersistenceUnit.datasource.isPresent()
- || DataSourceUtil.isDefault(hibernateOrmConfig.defaultPersistenceUnit.datasource.get()))
+ && (!hibernateOrmConfig.defaultPersistenceUnit().datasource().isPresent()
+ || DataSourceUtil.isDefault(hibernateOrmConfig.defaultPersistenceUnit().datasource().get()))
&& !defaultJdbcDataSource.isPresent()) {
String persistenceUnitName = PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME;
String dataSourceName = DataSourceUtil.DEFAULT_DATASOURCE_NAME;
@@ -877,7 +840,7 @@ private void handleHibernateORMWithNoPersistenceXml(
DataSourceUtil.dataSourceNotConfigured(dataSourceName));
}
- for (Entry persistenceUnitEntry : hibernateOrmConfig.persistenceUnits
+ for (Entry persistenceUnitEntry : hibernateOrmConfig.namedPersistenceUnits()
.entrySet()) {
producePersistenceUnitDescriptorFromConfig(
hibernateOrmConfig, persistenceUnitEntry.getKey(), persistenceUnitEntry.getValue(),
@@ -936,54 +899,55 @@ private static void producePersistenceUnitDescriptorFromConfig(
descriptor.setTransactionType(PersistenceUnitTransactionType.JTA);
- MultiTenancyStrategy multiTenancyStrategy = getMultiTenancyStrategy(persistenceUnitConfig.multitenant);
+ MultiTenancyStrategy multiTenancyStrategy = getMultiTenancyStrategy(persistenceUnitConfig.multitenant());
collectDialectConfig(persistenceUnitName, persistenceUnitConfig,
dbKindMetadataBuildItems, jdbcDataSource, multiTenancyStrategy,
systemProperties, descriptor.getProperties()::setProperty, storageEngineCollector);
// Physical Naming Strategy
- persistenceUnitConfig.physicalNamingStrategy.ifPresent(
+ persistenceUnitConfig.physicalNamingStrategy().ifPresent(
namingStrategy -> descriptor.getProperties()
.setProperty(AvailableSettings.PHYSICAL_NAMING_STRATEGY, namingStrategy));
// Implicit Naming Strategy
- persistenceUnitConfig.implicitNamingStrategy.ifPresent(
+ persistenceUnitConfig.implicitNamingStrategy().ifPresent(
namingStrategy -> descriptor.getProperties()
.setProperty(AvailableSettings.IMPLICIT_NAMING_STRATEGY, namingStrategy));
// Metadata builder contributor
- persistenceUnitConfig.metadataBuilderContributor.ifPresent(
+ persistenceUnitConfig.metadataBuilderContributor().ifPresent(
className -> descriptor.getProperties()
.setProperty(EntityManagerFactoryBuilderImpl.METADATA_BUILDER_CONTRIBUTOR, className));
// Mapping
- if (persistenceUnitConfig.mapping.timezone.timeZoneDefaultStorage.isPresent()) {
+ if (persistenceUnitConfig.mapping().timezone().timeZoneDefaultStorage().isPresent()) {
descriptor.getProperties().setProperty(AvailableSettings.TIMEZONE_DEFAULT_STORAGE,
- persistenceUnitConfig.mapping.timezone.timeZoneDefaultStorage.get().name());
+ persistenceUnitConfig.mapping().timezone().timeZoneDefaultStorage().get().name());
}
descriptor.getProperties().setProperty(AvailableSettings.PREFERRED_POOLED_OPTIMIZER,
- persistenceUnitConfig.mapping.id.optimizer.idOptimizerDefault
+ persistenceUnitConfig.mapping().id().optimizer().idOptimizerDefault()
.orElse(HibernateOrmConfigPersistenceUnit.IdOptimizerType.POOLED_LO).configName);
//charset
descriptor.getProperties().setProperty(AvailableSettings.HBM2DDL_CHARSET_NAME,
- persistenceUnitConfig.database.charset.name());
+ persistenceUnitConfig.database().charset().name());
// Quoting strategy
- if (persistenceUnitConfig.quoteIdentifiers.strategy == IdentifierQuotingStrategy.ALL
- || persistenceUnitConfig.quoteIdentifiers.strategy == IdentifierQuotingStrategy.ALL_EXCEPT_COLUMN_DEFINITIONS
- || persistenceUnitConfig.database.globallyQuotedIdentifiers) {
+ if (persistenceUnitConfig.quoteIdentifiers().strategy() == IdentifierQuotingStrategy.ALL
+ || persistenceUnitConfig.quoteIdentifiers()
+ .strategy() == IdentifierQuotingStrategy.ALL_EXCEPT_COLUMN_DEFINITIONS
+ || persistenceUnitConfig.database().globallyQuotedIdentifiers()) {
descriptor.getProperties().setProperty(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, "true");
}
- if (persistenceUnitConfig.quoteIdentifiers.strategy == IdentifierQuotingStrategy.ALL_EXCEPT_COLUMN_DEFINITIONS) {
+ if (persistenceUnitConfig.quoteIdentifiers().strategy() == IdentifierQuotingStrategy.ALL_EXCEPT_COLUMN_DEFINITIONS) {
descriptor.getProperties().setProperty(
AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS_SKIP_COLUMN_DEFINITIONS, "true");
- } else if (persistenceUnitConfig.quoteIdentifiers.strategy == IdentifierQuotingStrategy.ONLY_KEYWORDS) {
+ } else if (persistenceUnitConfig.quoteIdentifiers().strategy() == IdentifierQuotingStrategy.ONLY_KEYWORDS) {
descriptor.getProperties().setProperty(AvailableSettings.KEYWORD_AUTO_QUOTING_ENABLED, "true");
}
// Query
- int batchSize = firstPresent(persistenceUnitConfig.fetch.batchSize, persistenceUnitConfig.batchFetchSize)
+ int batchSize = firstPresent(persistenceUnitConfig.fetch().batchSize(), persistenceUnitConfig.batchFetchSize())
.orElse(16);
if (batchSize > 0) {
descriptor.getProperties().setProperty(AvailableSettings.DEFAULT_BATCH_FETCH_SIZE,
@@ -991,49 +955,49 @@ private static void producePersistenceUnitDescriptorFromConfig(
descriptor.getProperties().setProperty(AvailableSettings.BATCH_FETCH_STYLE, BatchFetchStyle.PADDED.toString());
}
- if (persistenceUnitConfig.fetch.maxDepth.isPresent()) {
- setMaxFetchDepth(descriptor, persistenceUnitConfig.fetch.maxDepth);
- } else if (persistenceUnitConfig.maxFetchDepth.isPresent()) {
- setMaxFetchDepth(descriptor, persistenceUnitConfig.maxFetchDepth);
+ if (persistenceUnitConfig.fetch().maxDepth().isPresent()) {
+ setMaxFetchDepth(descriptor, persistenceUnitConfig.fetch().maxDepth());
+ } else if (persistenceUnitConfig.maxFetchDepth().isPresent()) {
+ setMaxFetchDepth(descriptor, persistenceUnitConfig.maxFetchDepth());
}
descriptor.getProperties().setProperty(AvailableSettings.QUERY_PLAN_CACHE_MAX_SIZE, Integer.toString(
- persistenceUnitConfig.query.queryPlanCacheMaxSize));
+ persistenceUnitConfig.query().queryPlanCacheMaxSize()));
descriptor.getProperties().setProperty(AvailableSettings.DEFAULT_NULL_ORDERING,
- persistenceUnitConfig.query.defaultNullOrdering.name().toLowerCase(Locale.ROOT));
+ persistenceUnitConfig.query().defaultNullOrdering().name().toLowerCase(Locale.ROOT));
descriptor.getProperties().setProperty(AvailableSettings.IN_CLAUSE_PARAMETER_PADDING,
- String.valueOf(persistenceUnitConfig.query.inClauseParameterPadding));
+ String.valueOf(persistenceUnitConfig.query().inClauseParameterPadding()));
// Disable sequence validations: they are reportedly slow, and people already get the same validation from normal schema validation
descriptor.getProperties().put(AvailableSettings.SEQUENCE_INCREMENT_SIZE_MISMATCH_STRATEGY,
SequenceMismatchStrategy.NONE);
// JDBC
- persistenceUnitConfig.jdbc.timezone.ifPresent(
+ persistenceUnitConfig.jdbc().timezone().ifPresent(
timezone -> descriptor.getProperties().setProperty(AvailableSettings.JDBC_TIME_ZONE, timezone));
- persistenceUnitConfig.jdbc.statementFetchSize.ifPresent(
+ persistenceUnitConfig.jdbc().statementFetchSize().ifPresent(
fetchSize -> descriptor.getProperties().setProperty(AvailableSettings.STATEMENT_FETCH_SIZE,
String.valueOf(fetchSize)));
- persistenceUnitConfig.jdbc.statementBatchSize.ifPresent(
+ persistenceUnitConfig.jdbc().statementBatchSize().ifPresent(
fetchSize -> descriptor.getProperties().setProperty(AvailableSettings.STATEMENT_BATCH_SIZE,
String.valueOf(fetchSize)));
// Statistics
- if (hibernateOrmConfig.metrics.enabled
- || (hibernateOrmConfig.statistics.isPresent() && hibernateOrmConfig.statistics.get())) {
+ if (hibernateOrmConfig.metrics().enabled()
+ || (hibernateOrmConfig.statistics().isPresent() && hibernateOrmConfig.statistics().get())) {
descriptor.getProperties().setProperty(AvailableSettings.GENERATE_STATISTICS, "true");
//When statistics are enabled, the default in Hibernate ORM is to also log them after each
// session; turn that off by default as it's very noisy:
descriptor.getProperties().setProperty(AvailableSettings.LOG_SESSION_METRICS,
- String.valueOf(hibernateOrmConfig.logSessionMetrics.orElse(false)));
+ String.valueOf(hibernateOrmConfig.logSessionMetrics().orElse(false)));
}
// sql-load-scripts
- List importFiles = getSqlLoadScript(persistenceUnitConfig.sqlLoadScript, launchMode);
+ List importFiles = getSqlLoadScript(persistenceUnitConfig.sqlLoadScript(), launchMode);
if (!importFiles.isEmpty()) {
for (String importFile : importFiles) {
@@ -1044,19 +1008,19 @@ private static void producePersistenceUnitDescriptorFromConfig(
throw new ConfigurationException(
"Unable to interpret path referenced in '"
+ HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitName, "sql-load-script") + "="
- + String.join(",", persistenceUnitConfig.sqlLoadScript.get())
+ + String.join(",", persistenceUnitConfig.sqlLoadScript().get())
+ "': " + e.getMessage());
}
if (loadScriptPath != null && !Files.isDirectory(loadScriptPath)) {
// enlist resource if present
nativeImageResources.produce(new NativeImageResourceBuildItem(importFile));
- } else if (persistenceUnitConfig.sqlLoadScript.isPresent()) {
+ } else if (persistenceUnitConfig.sqlLoadScript().isPresent()) {
//raise exception if explicit file is not present (i.e. not the default)
throw new ConfigurationException(
"Unable to find file referenced in '"
+ HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitName, "sql-load-script") + "="
- + String.join(",", persistenceUnitConfig.sqlLoadScript.get())
+ + String.join(",", persistenceUnitConfig.sqlLoadScript().get())
+ "'. Remove property or add file to your path.");
}
// in dev mode we want to make sure that we watch for changes to file even if it doesn't currently exist
@@ -1065,7 +1029,7 @@ private static void producePersistenceUnitDescriptorFromConfig(
}
// only set the found import files if configured
- if (persistenceUnitConfig.sqlLoadScript.isPresent()) {
+ if (persistenceUnitConfig.sqlLoadScript().isPresent()) {
descriptor.getProperties().setProperty(AvailableSettings.HBM2DDL_IMPORT_FILES, String.join(",", importFiles));
}
} else {
@@ -1074,7 +1038,7 @@ private static void producePersistenceUnitDescriptorFromConfig(
}
// Caching
- if (persistenceUnitConfig.secondLevelCachingEnabled) {
+ if (persistenceUnitConfig.secondLevelCachingEnabled()) {
Properties p = descriptor.getProperties();
//Only set these if the user isn't making an explicit choice:
p.putIfAbsent(USE_DIRECT_REFERENCE_CACHE_ENTRIES, Boolean.TRUE);
@@ -1097,7 +1061,7 @@ private static void producePersistenceUnitDescriptorFromConfig(
// Hibernate Validator integration: we force the callback mode to have bootstrap errors reported rather than validation ignored
// if there is any issue when bootstrapping Hibernate Validator.
if (capabilities.isPresent(Capability.HIBERNATE_VALIDATOR)) {
- if (persistenceUnitConfig.validation.enabled) {
+ if (persistenceUnitConfig.validation().enabled()) {
descriptor.getProperties().setProperty(AvailableSettings.JAKARTA_VALIDATION_MODE,
ValidationMode.CALLBACK.name());
} else {
@@ -1107,7 +1071,7 @@ private static void producePersistenceUnitDescriptorFromConfig(
// Discriminator Column
descriptor.getProperties().setProperty(AvailableSettings.IGNORE_EXPLICIT_DISCRIMINATOR_COLUMNS_FOR_JOINED_SUBCLASS,
- String.valueOf(persistenceUnitConfig.discriminator.ignoreExplicitForJoined));
+ String.valueOf(persistenceUnitConfig.discriminator().ignoreExplicitForJoined()));
persistenceUnitDescriptors.produce(
new PersistenceUnitDescriptorBuildItem(descriptor,
@@ -1117,9 +1081,9 @@ private static void producePersistenceUnitDescriptorFromConfig(
jdbcDataSource.map(JdbcDataSourceBuildItem::getDbKind),
jdbcDataSource.flatMap(JdbcDataSourceBuildItem::getDbVersion),
multiTenancyStrategy,
- hibernateOrmConfig.database.ormCompatibilityVersion,
- persistenceUnitConfig.unsupportedProperties),
- persistenceUnitConfig.multitenantSchemaDatasource.orElse(null),
+ hibernateOrmConfig.database().ormCompatibilityVersion(),
+ persistenceUnitConfig.unsupportedProperties()),
+ persistenceUnitConfig.multitenantSchemaDatasource().orElse(null),
xmlMappings,
false, false, capabilities));
}
@@ -1130,7 +1094,7 @@ private static void collectDialectConfig(String persistenceUnitName,
MultiTenancyStrategy multiTenancyStrategy,
BuildProducer systemProperties,
BiConsumer puPropertiesCollector, Set storageEngineCollector) {
- Optional explicitDialect = persistenceUnitConfig.dialect.dialect;
+ Optional explicitDialect = persistenceUnitConfig.dialect().dialect();
Optional dbKind = jdbcDataSource.map(JdbcDataSourceBuildItem::getDbKind);
Optional explicitDbMinVersion = jdbcDataSource.flatMap(JdbcDataSourceBuildItem::getDbVersion);
if (multiTenancyStrategy != MultiTenancyStrategy.DATABASE && jdbcDataSource.isEmpty()) {
@@ -1182,14 +1146,14 @@ private static void collectDialectConfig(String persistenceUnitName,
persistenceUnitName));
}
- if (persistenceUnitConfig.dialect.storageEngine.isPresent()) {
+ if (persistenceUnitConfig.dialect().storageEngine().isPresent()) {
// Only actually set the storage engines if MySQL or MariaDB
if (isMySQLOrMariaDB(dialect.get())) {
// The storage engine has to be set as a system property.
// We record it so that we can later run checks (because we can only set a single value)
- storageEngineCollector.add(persistenceUnitConfig.dialect.storageEngine.get());
+ storageEngineCollector.add(persistenceUnitConfig.dialect().storageEngine().get());
systemProperties.produce(new SystemPropertyBuildItem(AvailableSettings.STORAGE_ENGINE,
- persistenceUnitConfig.dialect.storageEngine.get()));
+ persistenceUnitConfig.dialect().storageEngine().get()));
} else {
LOG.warnf("The storage engine set through configuration property '%1$s' is being ignored"
+ " because the database is neither MySQL nor MariaDB.",
@@ -1224,8 +1188,8 @@ private static void collectDialectConfigForPersistenceXml(String persistenceUnit
private static Optional findJdbcDataSource(String persistenceUnitName,
HibernateOrmConfigPersistenceUnit persistenceUnitConfig, List jdbcDataSources) {
- if (persistenceUnitConfig.datasource.isPresent()) {
- String dataSourceName = persistenceUnitConfig.datasource.get();
+ if (persistenceUnitConfig.datasource().isPresent()) {
+ String dataSourceName = persistenceUnitConfig.datasource().get();
return Optional.of(jdbcDataSources.stream()
.filter(i -> dataSourceName.equals(i.getName()))
.findFirst()
@@ -1293,22 +1257,23 @@ public static Map> getModelClassesAndPackagesPerPersistenceU
// handle the default persistence unit
if (enableDefaultPersistenceUnit) {
- if (!hibernateOrmConfig.defaultPersistenceUnit.packages.isPresent()) {
+ if (!hibernateOrmConfig.defaultPersistenceUnit().packages().isPresent()) {
throw new ConfigurationException("Packages must be configured for the default persistence unit.");
}
- for (String packageName : hibernateOrmConfig.defaultPersistenceUnit.packages.get()) {
+ for (String packageName : hibernateOrmConfig.defaultPersistenceUnit().packages().get()) {
packageRules.computeIfAbsent(normalizePackage(packageName), p -> new HashSet<>())
.add(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME);
}
}
// handle the named persistence units
- for (Entry candidatePersistenceUnitEntry : hibernateOrmConfig.persistenceUnits
+ for (Entry candidatePersistenceUnitEntry : hibernateOrmConfig
+ .namedPersistenceUnits()
.entrySet()) {
String candidatePersistenceUnitName = candidatePersistenceUnitEntry.getKey();
- Set candidatePersistenceUnitPackages = candidatePersistenceUnitEntry.getValue().packages
+ Set candidatePersistenceUnitPackages = candidatePersistenceUnitEntry.getValue().packages()
.orElseThrow(() -> new ConfigurationException(String.format(Locale.ROOT,
"Packages must be configured for persistence unit '%s'.", candidatePersistenceUnitName)));
@@ -1333,7 +1298,7 @@ public static Map> getModelClassesAndPackagesPerPersistenceU
.add(persistenceUnitName);
}
}
- } else if (!hibernateOrmConfig.persistenceUnits.isEmpty()) {
+ } else if (!hibernateOrmConfig.namedPersistenceUnits().isEmpty()) {
throw new ConfigurationException(
"Multiple persistence units are defined but the entities are not mapped to them. You should either use the .packages Quarkus configuration property or package-level @PersistenceUnit annotations.");
} else {
@@ -1439,9 +1404,9 @@ private static String normalizePackage(String pakkage) {
}
private static boolean hasPackagesInQuarkusConfig(HibernateOrmConfig hibernateOrmConfig) {
- for (HibernateOrmConfigPersistenceUnit persistenceUnitConfig : hibernateOrmConfig.getAllPersistenceUnitConfigsAsMap()
+ for (HibernateOrmConfigPersistenceUnit persistenceUnitConfig : hibernateOrmConfig.persistenceUnits()
.values()) {
- if (persistenceUnitConfig.packages.isPresent()) {
+ if (persistenceUnitConfig.packages().isPresent()) {
return true;
}
}
@@ -1480,7 +1445,7 @@ private static Collection getPackageLevelPersistenceUnitAnno
* @return true if we're expected to ignore them
*/
private boolean shouldIgnorePersistenceXmlResources(HibernateOrmConfig config) {
- return config.persistenceXml.ignore || Boolean.getBoolean("SKIP_PARSE_PERSISTENCE_XML");
+ return config.persistenceXml().ignore() || Boolean.getBoolean("SKIP_PARSE_PERSISTENCE_XML");
}
/**
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevServicesProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevServicesProcessor.java
index c47ba9e5666077..a6c15baddbba7d 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevServicesProcessor.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevServicesProcessor.java
@@ -36,9 +36,9 @@ void devServicesAutoGenerateByDefault(List s
Set managedSources = schemaReadyBuildItems.stream().map(JdbcDataSourceSchemaReadyBuildItem::getDatasourceNames)
.collect(HashSet::new, Collection::addAll, Collection::addAll);
- for (Map.Entry entry : config.getAllPersistenceUnitConfigsAsMap()
+ for (Map.Entry entry : config.persistenceUnits()
.entrySet()) {
- Optional dataSourceName = entry.getValue().datasource;
+ Optional dataSourceName = entry.getValue().datasource();
List propertyKeysIndicatingDataSourceConfigured = DataSourceUtil
.dataSourcePropertyKeys(dataSourceName.orElse(null), "username");
diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/metrics/HibernateOrmMetricsProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/metrics/HibernateOrmMetricsProcessor.java
index 155c09b7e39573..d5567e1bca318c 100644
--- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/metrics/HibernateOrmMetricsProcessor.java
+++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/metrics/HibernateOrmMetricsProcessor.java
@@ -33,7 +33,7 @@ public void metrics(HibernateOrmConfig config,
// IF Hibernate metrics and Hibernate statistics are enabled
// then define a consumer. It will only be invoked if metrics is enabled
- if (config.metrics.enabled && config.statistics.orElse(true) && metricsConfiguration.isPresent()) {
+ if (config.metrics().enabled() && config.statistics().orElse(true) && metricsConfiguration.isPresent()) {
datasourceMetrics.produce(new MetricsFactoryConsumerBuildItem(metricsRecorder.consumeMetricsFactory()));
}
}
diff --git a/extensions/hibernate-orm/deployment/src/main/resources/dev-ui/hibernate-orm-persistence-units.js b/extensions/hibernate-orm/deployment/src/main/resources/dev-ui/hibernate-orm-persistence-units.js
index f28762193739a8..0d6e245238ad13 100644
--- a/extensions/hibernate-orm/deployment/src/main/resources/dev-ui/hibernate-orm-persistence-units.js
+++ b/extensions/hibernate-orm/deployment/src/main/resources/dev-ui/hibernate-orm-persistence-units.js
@@ -5,16 +5,16 @@ import '@vaadin/button';
import '@vaadin/grid';
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
import { notifier } from 'notifier';
+import { observeState } from 'lit-element-state';
+import { themeState } from 'theme-state';
+import '@quarkus-webcomponents/codeblock';
-export class HibernateOrmPersistenceUnitsComponent extends LitElement {
+export class HibernateOrmPersistenceUnitsComponent extends observeState(LitElement) {
static styles = css`
.full-height {
height: 100%;
}
- .ddl-script {
- padding: 5px;
- }
a.script-heading {
display: block;
float:left;
@@ -81,7 +81,11 @@ export class HibernateOrmPersistenceUnitsComponent extends LitElement {
-
- * If your select query does not start with from, we support the following additional forms:
+ * If your select query does not start with from, select, or with, we support the
+ * following additional forms:
*
*
*
order by ... which will expand to from EntityName order by ...
- *
<singleColumnName> (and single parameter) which will expand to
- * from EntityName where <singleColumnName> = ?
+ *
<singleAttribute> (and single parameter) which will expand to
+ * from EntityName where <singleAttribute> = ?
+ *
where <query> will expand to from EntityName where <query>
*
<query> will expand to from EntityName where <query>
*
*
+ *
* If your update query does not start with update from, we support the following additional forms:
*
*
*
from EntityName ... which will expand to update from EntityName ...
- *
set? <singleColumnName> (and single parameter) which will expand to
- * update from EntityName set <singleColumnName> = ?
+ *
set? <singleAttribute> (and single parameter) which will expand to
+ * update from EntityName set <singleAttribute> = ?
*
set? <update-query> will expand to
* update from EntityName set <update-query> = ?
*
*
+ *
* If your delete query does not start with delete from, we support the following additional forms:
*
*
*
from EntityName ... which will expand to delete from EntityName ...
- *
<singleColumnName> (and single parameter) which will expand to
- * delete from EntityName where <singleColumnName> = ?
+ *
<singleAttribute> (and single parameter) which will expand to
+ * delete from EntityName where <singleAttribute> = ?
*
<query> will expand to delete from EntityName where <query>
*
*
diff --git a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/binder/MongoParserVisitor.java b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/binder/MongoParserVisitor.java
index 7af3a4cfbc5d96..fc3294487c00bf 100644
--- a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/binder/MongoParserVisitor.java
+++ b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/binder/MongoParserVisitor.java
@@ -3,6 +3,13 @@
import java.util.Map;
import io.quarkus.panacheql.internal.HqlParser;
+import io.quarkus.panacheql.internal.HqlParser.ComparisonPredicateContext;
+import io.quarkus.panacheql.internal.HqlParser.GroupedExpressionContext;
+import io.quarkus.panacheql.internal.HqlParser.GroupedPredicateContext;
+import io.quarkus.panacheql.internal.HqlParser.NamedParameterContext;
+import io.quarkus.panacheql.internal.HqlParser.ParameterContext;
+import io.quarkus.panacheql.internal.HqlParser.PositionalParameterContext;
+import io.quarkus.panacheql.internal.HqlParser.StandardFunctionContext;
import io.quarkus.panacheql.internal.HqlParserBaseVisitor;
class MongoParserVisitor extends HqlParserBaseVisitor {
@@ -38,18 +45,28 @@ public String visitOrPredicate(HqlParser.OrPredicateContext ctx) {
}
@Override
- public String visitEqualityPredicate(HqlParser.EqualityPredicateContext ctx) {
- return ctx.expression(0).accept(this) + ":" + ctx.expression(1).accept(this);
- }
-
- @Override
- public String visitInequalityPredicate(HqlParser.InequalityPredicateContext ctx) {
- return ctx.expression(0).accept(this) + ":{'$ne':" + ctx.expression(1).accept(this) + "}";
- }
-
- @Override
- public String visitLessThanOrEqualPredicate(HqlParser.LessThanOrEqualPredicateContext ctx) {
- return ctx.expression(0).accept(this) + ":{'$lte':" + ctx.expression(1).accept(this) + "}";
+ public String visitComparisonPredicate(ComparisonPredicateContext ctx) {
+ String lhs = ctx.expression(0).accept(this);
+ String rhs = ctx.expression(1).accept(this);
+ if (ctx.comparisonOperator().EQUAL() != null) {
+ return lhs + ":" + rhs;
+ }
+ if (ctx.comparisonOperator().NOT_EQUAL() != null) {
+ return lhs + ":{'$ne':" + rhs + "}";
+ }
+ if (ctx.comparisonOperator().GREATER() != null) {
+ return lhs + ":{'$gt':" + rhs + "}";
+ }
+ if (ctx.comparisonOperator().GREATER_EQUAL() != null) {
+ return lhs + ":{'$gte':" + rhs + "}";
+ }
+ if (ctx.comparisonOperator().LESS() != null) {
+ return lhs + ":{'$lt':" + rhs + "}";
+ }
+ if (ctx.comparisonOperator().LESS_EQUAL() != null) {
+ return lhs + ":{'$lte':" + rhs + "}";
+ }
+ return super.visitComparisonPredicate(ctx);
}
@Override
@@ -64,33 +81,47 @@ public String visitLikePredicate(HqlParser.LikePredicateContext ctx) {
}
@Override
- public String visitGreaterThanPredicate(HqlParser.GreaterThanPredicateContext ctx) {
- return ctx.expression(0).accept(this) + ":{'$gt':" + ctx.expression(1).accept(this) + "}";
+ public String visitIsNullPredicate(HqlParser.IsNullPredicateContext ctx) {
+ boolean exists = ctx.NOT() != null;
+ return ctx.expression().accept(this) + ":{'$exists':" + exists + "}";
+ }
+
+ @Override
+ public String visitLiteralExpression(HqlParser.LiteralExpressionContext ctx) {
+ String text = ctx.getText();
+ // FIXME: this only really supports text literals
+ if (ctx.literal().STRING_LITERAL() != null) {
+ text = text.substring(1, text.length() - 1);
+ }
+ return CommonQueryBinder.escape(text);
}
@Override
- public String visitLessThanPredicate(HqlParser.LessThanPredicateContext ctx) {
- return ctx.expression(0).accept(this) + ":{'$lt':" + ctx.expression(1).accept(this) + "}";
+ public String visitNamedParameter(NamedParameterContext ctx) {
+ return visitParameter(ctx);
}
@Override
- public String visitGreaterThanOrEqualPredicate(HqlParser.GreaterThanOrEqualPredicateContext ctx) {
- return ctx.expression(0).accept(this) + ":{'$gte':" + ctx.expression(1).accept(this) + "}";
+ public String visitPositionalParameter(PositionalParameterContext ctx) {
+ return visitParameter(ctx);
}
@Override
- public String visitIsNullPredicate(HqlParser.IsNullPredicateContext ctx) {
- boolean exists = ctx.NOT() != null;
- return ctx.expression().accept(this) + ":{'$exists':" + exists + "}";
+ public String visitParameterExpression(HqlParser.ParameterExpressionContext ctx) {
+ return visitParameter(ctx.parameter());
}
@Override
- public String visitLiteralExpression(HqlParser.LiteralExpressionContext ctx) {
- return CommonQueryBinder.escape(ctx.getText());
+ public String visitGroupedExpression(GroupedExpressionContext ctx) {
+ return ctx.expression().accept(this);
}
@Override
- public String visitParameterExpression(HqlParser.ParameterExpressionContext ctx) {
+ public String visitGroupedPredicate(GroupedPredicateContext ctx) {
+ return ctx.predicate().accept(this);
+ }
+
+ private String visitParameter(ParameterContext ctx) {
// this will match parameters used by PanacheQL : '?1' for index based or ':key' for named one.
if (parameterMaps.containsKey(ctx.getText())) {
Object value = parameterMaps.get(ctx.getText());
@@ -102,9 +133,20 @@ public String visitParameterExpression(HqlParser.ParameterExpressionContext ctx)
}
@Override
- public String visitPathExpression(HqlParser.PathExpressionContext ctx) {
+ public String visitGeneralPathExpression(HqlParser.GeneralPathExpressionContext ctx) {
+ String identifier = unquote(ctx.getText());
// this is the name of the field, we apply replacement and escape with '
- return "'" + replacementMap.getOrDefault(ctx.getText(), ctx.getText()) + "'";
+ return "'" + replacementMap.getOrDefault(identifier, identifier) + "'";
+ }
+
+ /**
+ * Removes backticks for quoted identifiers
+ */
+ private String unquote(String text) {
+ if (text.startsWith("`") && text.endsWith("`") && text.length() >= 2) {
+ return text.substring(1, text.length() - 1);
+ }
+ return text;
}
@Override
@@ -115,4 +157,10 @@ public String visitInPredicate(HqlParser.InPredicateContext ctx) {
.append("]}");
return sb.toString();
}
+
+ // Turn new date functions such as instant into regular fields, to not break existing queries
+ @Override
+ public String visitStandardFunction(StandardFunctionContext ctx) {
+ return "'" + ctx.getText() + "'";
+ }
}
diff --git a/extensions/panache/mongodb-panache-common/runtime/src/test/java/io/quarkus/mongodb/panache/common/runtime/MongoOperationsTest.java b/extensions/panache/mongodb-panache-common/runtime/src/test/java/io/quarkus/mongodb/panache/common/runtime/MongoOperationsTest.java
index 1b84561ffdd26c..14089ccafae324 100644
--- a/extensions/panache/mongodb-panache-common/runtime/src/test/java/io/quarkus/mongodb/panache/common/runtime/MongoOperationsTest.java
+++ b/extensions/panache/mongodb-panache-common/runtime/src/test/java/io/quarkus/mongodb/panache/common/runtime/MongoOperationsTest.java
@@ -95,6 +95,14 @@ public void testBindShorthandFilter() {
//test field replacement
query = operations.bindFilter(DemoObj.class, "property", new Object[] { "a value" });
assertEquals("{'value':'a value'}", query);
+
+ // keywords (quoted)
+ query = operations.bindFilter(Object.class, "`instant` = ?1", new Object[] { "a value" });
+ assertEquals("{'instant':'a value'}", query);
+
+ // keywords (unquoted)
+ query = operations.bindFilter(Object.class, "instant = ?1", new Object[] { "a value" });
+ assertEquals("{'instant':'a value'}", query);
}
private Object toDate(LocalDateTime of) {
@@ -285,12 +293,12 @@ public void testBindEnhancedFilterByIndex() {
assertEquals("{'field':{'$in':['f1', 'f2']},'isOk':true}", query);
query = operations.bindFilter(DemoObj.class,
- "field in ?1 and property = ?2 or property = ?3",
+ "field in ?1 and (property = ?2 or property = ?3)",
new Object[] { list, "jpg", "gif" });
assertEquals("{'field':{'$in':['f1', 'f2']},'$or':[{'value':'jpg'},{'value':'gif'}]}", query);
query = operations.bindFilter(DemoObj.class,
- "field in ?1 and isOk = ?2 and property = ?3 or property = ?4",
+ "field in ?1 and isOk = ?2 and (property = ?3 or property = ?4)",
new Object[] { list, true, "jpg", "gif" });
assertEquals("{'field':{'$in':['f1', 'f2']},'isOk':true,'$or':[{'value':'jpg'},{'value':'gif'}]}", query);
}
@@ -361,12 +369,12 @@ public void testBindEnhancedFilterByName() {
assertEquals("{'field':{'$in':['f1', 'f2']},'isOk':true}", query);
query = operations.bindFilter(DemoObj.class,
- "field in :fields and property = :p1 or property = :p2",
+ "field in :fields and (property = :p1 or property = :p2)",
Parameters.with("fields", list).and("p1", "jpg").and("p2", "gif").map());
assertEquals("{'field':{'$in':['f1', 'f2']},'$or':[{'value':'jpg'},{'value':'gif'}]}", query);
query = operations.bindFilter(DemoObj.class,
- "field in :fields and isOk = :isOk and property = :p1 or property = :p2",
+ "field in :fields and isOk = :isOk and (property = :p1 or property = :p2)",
Parameters.with("fields", list)
.and("isOk", true)
.and("p1", "jpg")
diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/Panache.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/Panache.kt
index 817c55f54bd434..6bf79027759365 100644
--- a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/Panache.kt
+++ b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/Panache.kt
@@ -1,6 +1,6 @@
package io.quarkus.mongodb.panache.kotlin
-import com.mongodb.session.ClientSession
+import com.mongodb.client.ClientSession
import io.quarkus.mongodb.panache.kotlin.runtime.KotlinMongoOperations
object Panache {
diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/Panache.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/Panache.java
index 2e1f900dee3675..e84fa4cb2201dc 100644
--- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/Panache.java
+++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/Panache.java
@@ -1,6 +1,6 @@
package io.quarkus.mongodb.panache;
-import com.mongodb.session.ClientSession;
+import com.mongodb.client.ClientSession;
import io.quarkus.mongodb.panache.runtime.JavaMongoOperations;
diff --git a/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Sort.java b/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Sort.java
index d19ca3a7904d2b..a503da211661c4 100644
--- a/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Sort.java
+++ b/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Sort.java
@@ -100,6 +100,7 @@ public void setNullPrecedence(NullPrecedence nullPrecedence) {
}
private List columns = new ArrayList<>();
+ private boolean escapingEnabled = true;
private Sort() {
}
@@ -293,6 +294,16 @@ public Sort and(String name, Direction direction, NullPrecedence nullPrecedence)
return this;
}
+ /**
+ * Disables escaping of column names with a backticks during HQL Order By clause generation
+ *
+ * @return this instance, modified.
+ */
+ public Sort disableEscaping() {
+ escapingEnabled = false;
+ return this;
+ }
+
/**
* Get the sort columns
*
@@ -311,4 +322,8 @@ public List getColumns() {
public static Sort empty() {
return by();
}
+
+ public boolean isEscapingEnabled() {
+ return escapingEnabled;
+ }
}
diff --git a/extensions/panache/panache-hibernate-common/runtime/pom.xml b/extensions/panache/panache-hibernate-common/runtime/pom.xml
index dd0b52b346c16d..d2adbad237ab9b 100644
--- a/extensions/panache/panache-hibernate-common/runtime/pom.xml
+++ b/extensions/panache/panache-hibernate-common/runtime/pom.xml
@@ -24,6 +24,14 @@
io.quarkusquarkus-panache-common
+
+ org.hibernate.orm
+ hibernate-core
+
+
+ org.antlr
+ antlr4-runtime
+ jakarta.persistencejakarta.persistence-api
diff --git a/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/CountParserVisitor.java b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/CountParserVisitor.java
new file mode 100644
index 00000000000000..5ac893a4545c6c
--- /dev/null
+++ b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/CountParserVisitor.java
@@ -0,0 +1,108 @@
+package io.quarkus.panache.hibernate.common.runtime;
+
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.hibernate.grammars.hql.HqlParser.JoinContext;
+import org.hibernate.grammars.hql.HqlParser.QueryContext;
+import org.hibernate.grammars.hql.HqlParser.QueryOrderContext;
+import org.hibernate.grammars.hql.HqlParser.SelectClauseContext;
+import org.hibernate.grammars.hql.HqlParser.SimpleQueryGroupContext;
+import org.hibernate.grammars.hql.HqlParserBaseVisitor;
+
+public class CountParserVisitor extends HqlParserBaseVisitor {
+
+ private int inSimpleQueryGroup;
+ private StringBuilder sb = new StringBuilder();
+
+ @Override
+ public String visitSimpleQueryGroup(SimpleQueryGroupContext ctx) {
+ inSimpleQueryGroup++;
+ try {
+ return super.visitSimpleQueryGroup(ctx);
+ } finally {
+ inSimpleQueryGroup--;
+ }
+ }
+
+ @Override
+ public String visitQuery(QueryContext ctx) {
+ super.visitQuery(ctx);
+ if (inSimpleQueryGroup == 1 && ctx.selectClause() == null) {
+ // insert a count because there's no select
+ sb.append(" select count( * )");
+ }
+ return null;
+ }
+
+ @Override
+ public String visitSelectClause(SelectClauseContext ctx) {
+ if (inSimpleQueryGroup == 1) {
+ if (ctx.SELECT() != null) {
+ ctx.SELECT().accept(this);
+ }
+ if (ctx.DISTINCT() != null) {
+ sb.append(" count(");
+ ctx.DISTINCT().accept(this);
+ if (ctx.selectionList().children.size() != 1) {
+ // FIXME: error message should include query
+ throw new RuntimeException("Cannot count on more than one column");
+ }
+ ctx.selectionList().children.get(0).accept(this);
+ sb.append(" )");
+ } else {
+ sb.append(" count( * )");
+ }
+ } else {
+ super.visitSelectClause(ctx);
+ }
+ return null;
+ }
+
+ @Override
+ public String visitJoin(JoinContext ctx) {
+ if (inSimpleQueryGroup == 1 && ctx.FETCH() != null) {
+ // ignore fetch joins for main query
+ return null;
+ }
+ return super.visitJoin(ctx);
+ }
+
+ @Override
+ public String visitQueryOrder(QueryOrderContext ctx) {
+ if (inSimpleQueryGroup == 1) {
+ // ignore order/limit/offset for main query
+ return null;
+ }
+ return super.visitQueryOrder(ctx);
+ }
+
+ @Override
+ public String visitTerminal(TerminalNode node) {
+ append(node.getText());
+ return null;
+ }
+
+ @Override
+ protected String defaultResult() {
+ return null;
+ }
+
+ @Override
+ protected String aggregateResult(String aggregate, String nextResult) {
+ if (nextResult != null) {
+ append(nextResult);
+ }
+ return null;
+ }
+
+ private void append(String nextResult) {
+ // don't add space at start, or around dots
+ if (!sb.isEmpty() && sb.charAt(sb.length() - 1) != '.' && !nextResult.equals(".")) {
+ sb.append(" ");
+ }
+ sb.append(nextResult);
+ }
+
+ public String result() {
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java
index 3029a33398242c..13230c3c28c2de 100644
--- a/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java
+++ b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java
@@ -3,6 +3,12 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.hibernate.grammars.hql.HqlLexer;
+import org.hibernate.grammars.hql.HqlParser;
+import org.hibernate.grammars.hql.HqlParser.SelectStatementContext;
+
import io.quarkus.panache.common.Sort;
import io.quarkus.panache.common.exception.PanacheQueryException;
@@ -17,10 +23,32 @@ public class PanacheJpaUtil {
static final Pattern FROM_PATTERN = Pattern.compile("^\\s*FROM\\s+.*",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
- public static String getCountQuery(String query) {
+ // match a FETCH
+ static final Pattern FETCH_PATTERN = Pattern.compile(".*\\s+FETCH\\s+.*",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+
+ // match a lone SELECT
+ static final Pattern LONE_SELECT_PATTERN = Pattern.compile(".*SELECT\\s+.*",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+
+ // match a leading WITH
+ static final Pattern WITH_PATTERN = Pattern.compile("^\\s*WITH\\s+.*",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+
+ /**
+ * This turns an HQL (already expanded from Panache-QL) query into a count query, using text manipulation
+ * if we can, because it's faster, or fall back to using the ORM HQL parser in {@link #getCountQueryUsingParser(String)}
+ */
+ public static String getFastCountQuery(String query) {
// try to generate a good count query from the existing query
- Matcher selectMatcher = SELECT_PATTERN.matcher(query);
String countQuery;
+ // there are no fast ways to get rid of fetches, or WITH
+ if (FETCH_PATTERN.matcher(query).matches()
+ || WITH_PATTERN.matcher(query).matches()) {
+ return getCountQueryUsingParser(query);
+ }
+ // if it starts with select, we can optimise
+ Matcher selectMatcher = SELECT_PATTERN.matcher(query);
if (selectMatcher.matches()) {
// this one cannot be null
String firstSelection = selectMatcher.group(1).trim();
@@ -36,6 +64,9 @@ public static String getCountQuery(String query) {
// it's not distinct, forget the column list
countQuery = "SELECT COUNT(*) " + selectMatcher.group(3);
}
+ } else if (LONE_SELECT_PATTERN.matcher(query).matches()) {
+ // a select anywhere else in there might be tricky
+ return getCountQueryUsingParser(query);
} else if (FROM_PATTERN.matcher(query).matches()) {
countQuery = "SELECT COUNT(*) " + query;
} else {
@@ -51,6 +82,20 @@ public static String getCountQuery(String query) {
return countQuery;
}
+ /**
+ * This turns an HQL (already expanded from Panache-QL) query into a count query, using the
+ * ORM HQL parser. Slow version, see {@link #getFastCountQuery(String)} for the fast version.
+ */
+ public static String getCountQueryUsingParser(String query) {
+ HqlLexer lexer = new HqlLexer(CharStreams.fromString(query));
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ HqlParser parser = new HqlParser(tokens);
+ SelectStatementContext statement = parser.selectStatement();
+ CountParserVisitor visitor = new CountParserVisitor();
+ statement.accept(visitor);
+ return visitor.result();
+ }
+
public static String getEntityName(Class> entityClass) {
// FIXME: not true?
return entityClass.getName();
@@ -67,10 +112,13 @@ public static String createFindQuery(Class> entityClass, String query, int par
}
String trimmedLc = trimmed.toLowerCase();
- if (trimmedLc.startsWith("from ") || trimmedLc.startsWith("select ")) {
+ if (trimmedLc.startsWith("from ")
+ || trimmedLc.startsWith("select ")
+ || trimmedLc.startsWith("with ")) {
return query;
}
- if (trimmedLc.startsWith("order by ")) {
+ if (trimmedLc.startsWith("order by ")
+ || trimmedLc.startsWith("where ")) {
return "FROM " + getEntityName(entityClass) + " " + query;
}
if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1 && paramCount == 1) {
@@ -95,9 +143,17 @@ public static String createCountQuery(Class> entityClass, String query, int pa
return "SELECT COUNT(*) FROM " + getEntityName(entityClass);
String trimmedLc = trimmed.toLowerCase();
+ // assume these have valid select clauses and let them through
+ if (trimmedLc.startsWith("select ")
+ || trimmedLc.startsWith("with ")) {
+ return query;
+ }
if (trimmedLc.startsWith("from ")) {
return "SELECT COUNT(*) " + query;
}
+ if (trimmedLc.startsWith("where ")) {
+ return "SELECT COUNT(*) FROM " + getEntityName(entityClass) + " " + query;
+ }
if (trimmedLc.startsWith("order by ")) {
// ignore it
return "SELECT COUNT(*) FROM " + getEntityName(entityClass);
@@ -175,7 +231,11 @@ public static String toOrderBy(Sort sort) {
Sort.Column column = sort.getColumns().get(i);
if (i > 0)
sb.append(" , ");
- sb.append(column.getName());
+ if (sort.isEscapingEnabled()) {
+ sb.append(escapeColumnName(column.getName()));
+ } else {
+ sb.append(column.getName());
+ }
if (column.getDirection() != Sort.Direction.Ascending) {
sb.append(" DESC");
}
@@ -191,4 +251,30 @@ public static String toOrderBy(Sort sort) {
}
return sb.toString();
}
+
+ private static StringBuilder escapeColumnName(String columnName) {
+ StringBuilder sb = new StringBuilder();
+ String[] path = columnName.split("\\.");
+ for (int j = 0; j < path.length; j++) {
+ if (j > 0)
+ sb.append('.');
+ sb.append('`').append(unquoteColumnName(path[j])).append('`');
+ }
+ return sb;
+ }
+
+ private static String unquoteColumnName(String columnName) {
+ String unquotedColumnName;
+ //Note HQL uses backticks to escape/quote special words that are used as identifiers
+ if (columnName.charAt(0) == '`' && columnName.charAt(columnName.length() - 1) == '`') {
+ unquotedColumnName = columnName.substring(1, columnName.length() - 1);
+ } else {
+ unquotedColumnName = columnName;
+ }
+ // Note we're not dealing with columns but with entity attributes so no backticks expected in unquoted column name
+ if (unquotedColumnName.indexOf('`') >= 0) {
+ throw new PanacheQueryException("Sort column name cannot have backticks");
+ }
+ return unquotedColumnName;
+ }
}
diff --git a/extensions/panache/panache-hibernate-common/runtime/src/test/java/io/quarkus/panache/hibernate/common/runtime/CountTest.java b/extensions/panache/panache-hibernate-common/runtime/src/test/java/io/quarkus/panache/hibernate/common/runtime/CountTest.java
new file mode 100644
index 00000000000000..d285867a8b3fb2
--- /dev/null
+++ b/extensions/panache/panache-hibernate-common/runtime/src/test/java/io/quarkus/panache/hibernate/common/runtime/CountTest.java
@@ -0,0 +1,80 @@
+package io.quarkus.panache.hibernate.common.runtime;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class CountTest {
+ @Test
+ public void testParser() {
+ // one column, order/limit/offset
+ assertCountQueryUsingParser("select count( * ) from bar", "select foo from bar order by foo, bar ASC limit 2 offset 3");
+ // two columns
+ assertCountQueryUsingParser("select count( * ) from bar", "select foo,gee from bar");
+ // one column distinct
+ assertCountQueryUsingParser("select count( distinct foo ) from bar", "select distinct foo from bar");
+ // two columns distinct
+ Assertions.assertThrows(RuntimeException.class,
+ () -> assertCountQueryUsingParser("XX", "select distinct foo,gee from bar"));
+ // nested order by not touched
+ assertCountQueryUsingParser("select count( * ) from ( from entity order by id )",
+ "select foo from (from entity order by id) order by foo, bar ASC");
+ // what happens to literals?
+ assertCountQueryUsingParser("select count( * ) from bar where some = 2 and other = '23'",
+ "select foo from bar where some = 2 and other = '23'");
+ // fetches are gone
+ assertCountQueryUsingParser("select count( * ) from bar b", "select foo from bar b left join fetch b.things");
+ // non-fetches remain
+ assertCountQueryUsingParser("select count( * ) from bar b left join b.things",
+ "select foo from bar b left join b.things");
+
+ // inverted select
+ assertCountQueryUsingParser("from bar select count( * )", "from bar select foo");
+ // from without select
+ assertCountQueryUsingParser("from bar select count( * )", "from bar");
+
+ // CTE
+ assertFastCountQuery("WITH id AS ( SELECT p.id AS pid FROM Person2 AS p ) SELECT count( * ) FROM Person2 p",
+ "WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT p FROM Person2 p");
+ }
+
+ @Test
+ public void testFastVersion() {
+ // one column, order/limit/offset
+ assertFastCountQuery("SELECT COUNT(*) from bar", "select foo from bar order by foo, bar ASC limit 2 offset 3");
+ // two columns
+ assertFastCountQuery("SELECT COUNT(*) from bar", "select foo,gee from bar");
+ // one column distinct
+ assertFastCountQuery("SELECT COUNT(distinct foo) from bar", "select distinct foo from bar");
+ // two columns distinct
+ Assertions.assertThrows(RuntimeException.class, () -> assertFastCountQuery("XX", "select distinct foo,gee from bar"));
+ // nested order by not touched
+ assertFastCountQuery("SELECT COUNT(*) from (from entity order by id)",
+ "select foo from (from entity order by id) order by foo, bar ASC");
+ // what happens to literals?
+ assertFastCountQuery("SELECT COUNT(*) from bar where some = 2 and other = '23'",
+ "select foo from bar where some = 2 and other = '23'");
+ // fetches are gone
+ assertFastCountQuery("select count( * ) from bar b", "select foo from bar b left join fetch b.things");
+ // non-fetches remain
+ assertFastCountQuery("SELECT COUNT(*) from bar b left join b.things", "select foo from bar b left join b.things");
+
+ // inverted select
+ assertFastCountQuery("from bar select count( * )", "from bar select foo");
+ // from without select
+ assertFastCountQuery("SELECT COUNT(*) from bar", "from bar");
+
+ // CTE
+ assertFastCountQuery("WITH id AS ( SELECT p.id AS pid FROM Person2 AS p ) SELECT count( * ) FROM Person2 p",
+ "WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT p FROM Person2 p");
+ }
+
+ private void assertCountQueryUsingParser(String expected, String selectQuery) {
+ String countQuery = PanacheJpaUtil.getCountQueryUsingParser(selectQuery);
+ Assertions.assertEquals(expected, countQuery);
+ }
+
+ private void assertFastCountQuery(String expected, String selectQuery) {
+ String countQuery = PanacheJpaUtil.getFastCountQuery(selectQuery);
+ Assertions.assertEquals(expected, countQuery);
+ }
+}
diff --git a/extensions/panache/panache-mock/pom.xml b/extensions/panache/panache-mock/pom.xml
index d33d2536e2e663..d6db8863936a46 100644
--- a/extensions/panache/panache-mock/pom.xml
+++ b/extensions/panache/panache-mock/pom.xml
@@ -13,10 +13,6 @@
Quarkus - Panache - MockMocking with Panache
-
- true
-
-
io.quarkus
@@ -47,6 +43,31 @@
-proc:none
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce
+
+
+
+
+ classpath:enforcer-rules/quarkus-banned-dependencies.xml
+
+
+ classpath:enforcer-rules/quarkus-banned-dependencies-okhttp.xml
+
+
+
+
+
+
diff --git a/extensions/panache/panacheql/.gitignore b/extensions/panache/panacheql/.gitignore
new file mode 100644
index 00000000000000..26c2d26a8f47de
--- /dev/null
+++ b/extensions/panache/panacheql/.gitignore
@@ -0,0 +1,3 @@
+gen/**
+**/internal/gen/**
+/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.tokens
diff --git a/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4 b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4
index cff2b1708253a9..6e8e8636f5779f 100644
--- a/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4
+++ b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4
@@ -10,70 +10,95 @@ lexer grammar HqlLexer;
*/
}
-WS : ( ' ' | '\t' | '\f' | EOL ) -> skip;
+WS : WS_CHAR+ -> skip;
fragment
-EOL : [\r\n]+;
+WS_CHAR : [ \f\t\r\n];
-INTEGER_LITERAL : INTEGER_NUMBER ;
+COMMENT : '/*' (~'*' | '*' ~'/' )* '*/' -> skip;
fragment
-INTEGER_NUMBER : ('0' | '1'..'9' '0'..'9'*) ;
+DIGIT : [0-9];
-LONG_LITERAL : INTEGER_NUMBER ('l'|'L');
+fragment
+HEX_DIGIT : [0-9a-fA-F];
-BIG_INTEGER_LITERAL : INTEGER_NUMBER ('bi'|'BI') ;
+fragment
+EXPONENT : [eE] [+-]? DIGIT+;
-HEX_LITERAL : '0' ('x'|'X') HEX_DIGIT+ ('l'|'L')? ;
+fragment
+LONG_SUFFIX : [lL];
fragment
-HEX_DIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ;
+FLOAT_SUFFIX : [fF];
-OCTAL_LITERAL : '0' ('0'..'7')+ ('l'|'L')? ;
+fragment
+DOUBLE_SUFFIX : [dD];
-FLOAT_LITERAL : FLOATING_POINT_NUMBER ('f'|'F')? ;
+fragment
+BIG_DECIMAL_SUFFIX : [bB] [dD];
+
+fragment
+BIG_INTEGER_SUFFIX : [bB] [iI];
+
+// Although this is not 100% correct because this accepts leading zeros,
+// we stick to this because temporal literals use this rule for simplicity.
+// Since we don't support octal literals, this shouldn't really be a big issue
+fragment
+INTEGER_NUMBER
+ : DIGIT+
+ ;
fragment
FLOATING_POINT_NUMBER
- : ('0'..'9')+ '.' ('0'..'9')* EXPONENT?
- | '.' ('0'..'9')+ EXPONENT?
- | ('0'..'9')+ EXPONENT
- | ('0'..'9')+
+ : DIGIT+ '.' DIGIT* EXPONENT?
+ | '.' DIGIT+ EXPONENT?
+ | DIGIT+ EXPONENT
+ | DIGIT+
;
-DOUBLE_LITERAL : FLOATING_POINT_NUMBER ('d'|'D') ;
+INTEGER_LITERAL : INTEGER_NUMBER ('_' INTEGER_NUMBER)*;
-BIG_DECIMAL_LITERAL : FLOATING_POINT_NUMBER ('bd'|'BD') ;
+LONG_LITERAL : INTEGER_NUMBER ('_' INTEGER_NUMBER)* LONG_SUFFIX;
-fragment
-EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;
+FLOAT_LITERAL : FLOATING_POINT_NUMBER FLOAT_SUFFIX;
-CHARACTER_LITERAL
- : '\'' ( ESCAPE_SEQUENCE | ~('\''|'\\') ) '\'' {setText(getText().substring(1, getText().length()-1));}
- ;
+DOUBLE_LITERAL : FLOATING_POINT_NUMBER DOUBLE_SUFFIX?;
-STRING_LITERAL
- : '"' ( ESCAPE_SEQUENCE | ~('\\'|'"') )* '"' {setText(getText().substring(1, getText().length()-1));}
- | ('\'' ( ESCAPE_SEQUENCE | ~('\\'|'\'') )* '\'')+ {setText(getText().substring(1, getText().length()-1).replace("''", "'"));}
- ;
+BIG_INTEGER_LITERAL : INTEGER_NUMBER BIG_INTEGER_SUFFIX;
+
+BIG_DECIMAL_LITERAL : FLOATING_POINT_NUMBER BIG_DECIMAL_SUFFIX;
+
+HEX_LITERAL : '0' [xX] HEX_DIGIT+ LONG_SUFFIX?;
+
+fragment SINGLE_QUOTE : '\'';
+fragment DOUBLE_QUOTE : '"';
+
+STRING_LITERAL : SINGLE_QUOTE ( SINGLE_QUOTE SINGLE_QUOTE | ~('\'') )* SINGLE_QUOTE;
+
+JAVA_STRING_LITERAL
+ : DOUBLE_QUOTE ( ESCAPE_SEQUENCE | ~('"') )* DOUBLE_QUOTE
+ | [jJ] SINGLE_QUOTE ( ESCAPE_SEQUENCE | ~('\'') )* SINGLE_QUOTE
+ | [jJ] DOUBLE_QUOTE ( ESCAPE_SEQUENCE | ~('\'') )* DOUBLE_QUOTE
+ ;
+
+fragment BACKSLASH : '\\';
fragment
ESCAPE_SEQUENCE
- : '\\' ('b'|'t'|'n'|'f'|'r'|'\\"'|'\''|'\\')
- | UNICODE_ESCAPE
- | OCTAL_ESCAPE
+ : BACKSLASH [btnfr"']
+ | BACKSLASH UNICODE_ESCAPE
+ | BACKSLASH BACKSLASH
;
fragment
-OCTAL_ESCAPE
- : '\\' ('0'..'3') ('0'..'7') ('0'..'7')
- | '\\' ('0'..'7') ('0'..'7')
- | '\\' ('0'..'7')
+UNICODE_ESCAPE
+ : 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
;
-fragment
-UNICODE_ESCAPE
- : '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
+BINARY_LITERAL
+ : [xX] SINGLE_QUOTE (HEX_DIGIT HEX_DIGIT)* SINGLE_QUOTE
+ | [xX] DOUBLE_QUOTE (HEX_DIGIT HEX_DIGIT)* DOUBLE_QUOTE
;
// ESCAPE start tokens
@@ -100,7 +125,7 @@ PLUS : '+';
MINUS : '-';
ASTERISK : '*';
SLASH : '/';
-PERCENT : '%';
+PERCENT_OP : '%';
AMPERSAND : '&';
SEMICOLON : ';';
COLON : ':';
@@ -109,123 +134,197 @@ DOUBLE_PIPE : '||';
QUESTION_MARK : '?';
ARROW : '->';
+
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Keywords
-ABS : [aA] [bB] [sS];
-AS : [aA] [sS];
+
+ID : [iI][dD];
+VERSION : [vV] [eE] [rR] [sS] [iI] [oO] [nN];
+VERSIONED : [vV] [eE] [rR] [sS] [iI] [oO] [nN] [eE] [dD];
+NATURALID : [nN] [aA] [tT] [uU] [rR] [aA] [lL] [iI] [dD];
+FK : [fF] [kK];
+
ALL : [aA] [lL] [lL];
AND : [aA] [nN] [dD];
ANY : [aA] [nN] [yY];
+AS : [aA] [sS];
ASC : [aA] [sS] [cC];
AVG : [aA] [vV] [gG];
+BETWEEN : [bB] [eE] [tT] [wW] [eE] [eE] [nN];
+BOTH : [bB] [oO] [tT] [hH];
+BREADTH : [bB] [rR] [eE] [aA] [dD] [tT] [hH];
BY : [bB] [yY];
-BETWEEN : [bB] [eE] [tT] [wW] [eE] [eE] [nN];
-BIT_LENGTH : [bB] [iI] [tT] [_] [lL] [eE] [nN] [gG] [tT] [hH];
-BOTH : [bB] [oO] [tT] [hH];
-CASE : [cC] [aA] [sS] [eE];
-CAST : [cC] [aA] [sS] [tT];
-CHARACTER_LENGTH : [cC] [hH] [aA] [rR] [aA] [cC] [tT] [eE] [rR] '_' [lL] [eE] [nN] [gG] [tT] [hH];
-CLASS : [cC] [lL] [aA] [sS] [sS];
-COALESCE : [cC] [oO] [aA] [lL] [eE] [sS] [cC] [eE];
+CASE : [cC] [aA] [sS] [eE];
+CAST : [cC] [aA] [sS] [tT];
COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE];
-CONCAT : [cC] [oO] [nN] [cC] [aA] [tT];
COUNT : [cC] [oO] [uU] [nN] [tT];
+CROSS : [cC] [rR] [oO] [sS] [sS];
+CUBE : [cC] [uU] [bB] [eE];
+CURRENT : [cC] [uU] [rR] [rR] [eE] [nN] [tT];
CURRENT_DATE : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [dD] [aA] [tT] [eE];
+CURRENT_INSTANT : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [iI] [nN] [sS] [tT] [aA] [nN] [tT]; //deprecated legacy
CURRENT_TIME : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [tT] [iI] [mM] [eE];
CURRENT_TIMESTAMP : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [tT] [iI] [mM] [eE] [sS] [tT] [aA] [mM] [pP];
-CROSS : [cC] [rR] [oO] [sS] [sS];
+CYCLE : [cC] [yY] [cC] [lL] [eE];
+DATE : [dD] [aA] [tT] [eE];
+DATETIME : [dD] [aA] [tT] [eE] [tT] [iI] [mM] [eE];
DAY : [dD] [aA] [yY];
+DEFAULT : [dD] [eE] [fF] [aA] [uU] [lL] [tT];
DELETE : [dD] [eE] [lL] [eE] [tT] [eE];
+DEPTH : [dD] [eE] [pP] [tT] [hH];
DESC : [dD] [eE] [sS] [cC];
DISTINCT : [dD] [iI] [sS] [tT] [iI] [nN] [cC] [tT];
+ELEMENT : [eE] [lL] [eE] [mM] [eE] [nN] [tT];
ELEMENTS : [eE] [lL] [eE] [mM] [eE] [nN] [tT] [sS];
ELSE : [eE] [lL] [sS] [eE];
EMPTY : [eE] [mM] [pP] [tT] [yY];
END : [eE] [nN] [dD];
ENTRY : [eE] [nN] [tT] [rR] [yY];
+EPOCH : [eE] [pP] [oO] [cC] [hH];
+ERROR : [eE] [rR] [rR] [oO] [rR];
ESCAPE : [eE] [sS] [cC] [aA] [pP] [eE];
+EVERY : [eE] [vV] [eE] [rR] [yY];
+EXCEPT : [eE] [xX] [cC] [eE] [pP] [tT];
+EXCLUDE : [eE] [xX] [cC] [lL] [uU] [dD] [eE];
EXISTS : [eE] [xX] [iI] [sS] [tT] [sS];
EXTRACT : [eE] [xX] [tT] [rR] [aA] [cC] [tT];
FETCH : [fF] [eE] [tT] [cC] [hH];
+FILTER : [fF] [iI] [lL] [tT] [eE] [rR];
+FIRST : [fF] [iI] [rR] [sS] [tT];
+FOLLOWING : [fF] [oO] [lL] [lL] [oO] [wW] [iI] [nN] [gG];
+FOR : [fF] [oO] [rR];
+FORMAT : [fF] [oO] [rR] [mM] [aA] [tT];
FROM : [fF] [rR] [oO] [mM];
FULL : [fF] [uU] [lL] [lL];
FUNCTION : [fF] [uU] [nN] [cC] [tT] [iI] [oO] [nN];
GROUP : [gG] [rR] [oO] [uU] [pP];
+GROUPS : [gG] [rR] [oO] [uU] [pP] [sS];
HAVING : [hH] [aA] [vV] [iI] [nN] [gG];
HOUR : [hH] [oO] [uU] [rR];
+IGNORE : [iI] [gG] [nN] [oO] [rR] [eE];
+ILIKE : [iI] [lL] [iI] [kK] [eE];
IN : [iI] [nN];
INDEX : [iI] [nN] [dD] [eE] [xX];
+INDICES : [iI] [nN] [dD] [iI] [cC] [eE] [sS];
INNER : [iI] [nN] [nN] [eE] [rR];
INSERT : [iI] [nN] [sS] [eE] [rR] [tT];
+INSTANT : [iI] [nN] [sS] [tT] [aA] [nN] [tT];
+INTERSECT : [iI] [nN] [tT] [eE] [rR] [sS] [eE] [cC] [tT];
INTO : [iI] [nN] [tT] [oO];
IS : [iI] [sS];
JOIN : [jJ] [oO] [iI] [nN];
KEY : [kK] [eE] [yY];
+KEYS : [kK] [eE] [yY] [sS];
+LAST : [lL] [aA] [sS] [tT];
+LATERAL : [lL] [aA] [tT] [eE] [rR] [aA] [lL];
LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG];
LEFT : [lL] [eE] [fF] [tT];
-LENGTH : [lL] [eE] [nN] [gG] [tT] [hH];
-LIMIT : [lL] [iI] [mM] [iI] [tT];
LIKE : [lL] [iI] [kK] [eE];
+LIMIT : [lL] [iI] [mM] [iI] [tT];
LIST : [lL] [iI] [sS] [tT];
-LOCATE : [lL] [oO] [cC] [aA] [tT] [eE];
-LOWER : [lL] [oO] [wW] [eE] [rR];
+LISTAGG : [lL] [iI] [sS] [tT] [aA] [gG] [gG];
+LOCAL : [lL] [oO] [cC] [aA] [lL];
+LOCAL_DATE : [lL] [oO] [cC] [aA] [lL] '_' [dD] [aA] [tT] [eE];
+LOCAL_DATETIME : [lL] [oO] [cC] [aA] [lL] '_' [dD] [aA] [tT] [eE] [tT] [iI] [mM] [eE];
+LOCAL_TIME : [lL] [oO] [cC] [aA] [lL] '_' [tT] [iI] [mM] [eE];
MAP : [mM] [aA] [pP];
+MATERIALIZED : [mM] [aA] [tT] [eE] [rR] [iI] [aA] [lL] [iI] [zZ] [eE] [dD];
MAX : [mM] [aA] [xX];
MAXELEMENT : [mM] [aA] [xX] [eE] [lL] [eE] [mM] [eE] [nN] [tT];
MAXINDEX : [mM] [aA] [xX] [iI] [nN] [dD] [eE] [xX];
MEMBER : [mM] [eE] [mM] [bB] [eE] [rR];
+MICROSECOND : [mM] [iI] [cC] [rR] [oO] [sS] [eE] [cC] [oO] [nN] [dD];
+MILLISECOND : [mM] [iI] [lL] [lL] [iI] [sS] [eE] [cC] [oO] [nN] [dD];
MIN : [mM] [iI] [nN];
MINELEMENT : [mM] [iI] [nN] [eE] [lL] [eE] [mM] [eE] [nN] [tT];
MININDEX : [mM] [iI] [nN] [iI] [nN] [dD] [eE] [xX];
MINUTE : [mM] [iI] [nN] [uU] [tT] [eE];
-MOD : [mM] [oO] [dD];
MONTH : [mM] [oO] [nN] [tT] [hH];
+NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD];
NEW : [nN] [eE] [wW];
+NEXT : [nN] [eE] [xX] [tT];
+NO : [nN] [oO];
NOT : [nN] [oO] [tT];
-NULLIF : [nN] [uU] [lL] [lL] [iI] [fF];
+NULLS : [nN] [uU] [lL] [lL] [sS];
OBJECT : [oO] [bB] [jJ] [eE] [cC] [tT];
-OCTET_LENGTH : [oO] [cC] [tT] [eE] [tT] '_' [lL] [eE] [nN] [gG] [tT] [hH];
OF : [oO] [fF];
OFFSET : [oO] [fF] [fF] [sS] [eE] [tT];
+OFFSET_DATETIME : [oO] [fF] [fF] [sS] [eE] [tT] '_' [dD] [aA] [tT] [eE] [tT] [iI] [mM] [eE];
ON : [oO] [nN];
+ONLY : [oO] [nN] [lL] [yY];
OR : [oO] [rR];
ORDER : [oO] [rR] [dD] [eE] [rR];
+OTHERS : [oO] [tT] [hH] [eE] [rR] [sS];
OUTER : [oO] [uU] [tT] [eE] [rR];
+OVER : [oO] [vV] [eE] [rR];
+OVERFLOW : [oO] [vV] [eE] [rR] [fF] [lL] [oO] [wW];
+OVERLAY : [oO] [vV] [eE] [rR] [lL] [aA] [yY];
+PAD : [pP] [aA] [dD];
+PARTITION : [pP] [aA] [rR] [tT] [iI] [tT] [iI] [oO] [nN];
+PERCENT : [pP] [eE] [rR] [cC] [eE] [nN] [tT];
+PLACING : [pP] [lL] [aA] [cC] [iI] [nN] [gG];
POSITION : [pP] [oO] [sS] [iI] [tT] [iI] [oO] [nN];
+PRECEDING : [pP] [rR] [eE] [cC] [eE] [dD] [iI] [nN] [gG];
+QUARTER : [qQ] [uU] [aA] [rR] [tT] [eE] [rR];
+RANGE : [rR] [aA] [nN] [gG] [eE];
+RESPECT : [rR] [eE] [sS] [pP] [eE] [cC] [tT];
RIGHT : [rR] [iI] [gG] [hH] [tT];
+ROLLUP : [rR] [oO] [lL] [lL] [uU] [pP];
+ROW : [rR] [oO] [wW];
+ROWS : [rR] [oO] [wW] [sS];
+SEARCH : [sS] [eE] [aA] [rR] [cC] [hH];
SECOND : [sS] [eE] [cC] [oO] [nN] [dD];
SELECT : [sS] [eE] [lL] [eE] [cC] [tT];
SET : [sS] [eE] [tT];
SIZE : [sS] [iI] [zZ] [eE];
-SQRT : [sS] [qQ] [rR] [tT];
-STR : [sS] [tT] [rR];
+SOME : [sS] [oO] [mM] [eE];
SUBSTRING : [sS] [uU] [bB] [sS] [tT] [rR] [iI] [nN] [gG];
-SUBSTR : [sS] [uU] [bB] [sS] [tT] [rR];
-SUM : [sS] [uU] [mM];
+SUM : [sS] [uM] [mM];
THEN : [tT] [hH] [eE] [nN];
+TIES : [tT] [iI] [eE] [sS];
+TIME : [tT] [iI] [mM] [eE];
+TIMESTAMP : [tT] [iI] [mM] [eE] [sS] [tT] [aA] [mM] [pP];
TIMEZONE_HOUR : [tT] [iI] [mM] [eE] [zZ] [oO] [nN] [eE] '_' [hH] [oO] [uU] [rR];
TIMEZONE_MINUTE : [tT] [iI] [mM] [eE] [zZ] [oO] [nN] [eE] '_' [mM] [iI] [nN] [uU] [tT] [eE];
+TO : [tT] [oO];
TRAILING : [tT] [rR] [aA] [iI] [lL] [iI] [nN] [gG];
TREAT : [tT] [rR] [eE] [aA] [tT];
TRIM : [tT] [rR] [iI] [mM];
+TRUNC : [tT] [rR] [uU] [nN] [cC];
+TRUNCATE : [tT] [rR] [uU] [nN] [cC] [aA] [tT] [eE];
TYPE : [tT] [yY] [pP] [eE];
+UNBOUNDED : [uU] [nN] [bB] [oO] [uU] [nN] [dD] [eE] [dD];
+UNION : [uU] [nN] [iI] [oO] [nN];
UPDATE : [uU] [pP] [dD] [aA] [tT] [eE];
-UPPER : [uU] [pP] [pP] [eE] [rR];
+USING : [uU] [sS] [iI] [nN] [gG];
VALUE : [vV] [aA] [lL] [uU] [eE];
+VALUES : [vV] [aA] [lL] [uU] [eE] [sS];
+WEEK : [wW] [eE] [eE] [kK];
WHEN : [wW] [hH] [eE] [nN];
WHERE : [wW] [hH] [eE] [rR] [eE];
WITH : [wW] [iI] [tT] [hH];
+WITHIN : [wW] [iI] [tT] [hH] [iI] [nN];
+WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT];
YEAR : [yY] [eE] [aA] [rR];
+ZONED : [zZ] [oO] [nN] [eE] [dD];
// case-insensitive true, false and null recognition (split vote :)
TRUE : [tT] [rR] [uU] [eE];
FALSE : [fF] [aA] [lL] [sS] [eE];
NULL : [nN] [uU] [lL] [lL];
+
+fragment
+LETTER : [a-zA-Z\u0080-\ufffe_$];
+
// Identifiers
IDENTIFIER
- : ('a'..'z'|'A'..'Z'|'_'|'$'|'\u0080'..'\ufffe')('a'..'z'|'A'..'Z'|'_'|'$'|'0'..'9'|'\u0080'..'\ufffe')*
+ : LETTER (LETTER | DIGIT)*
;
+fragment
+BACKTICK : '`';
+
QUOTED_IDENTIFIER
- : '`' ( ESCAPE_SEQUENCE | ~('\\'|'`') )* '`'
+ : BACKTICK ( ESCAPE_SEQUENCE | '\\' BACKTICK | ~([`]) )* BACKTICK
;
diff --git a/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4 b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4
index 25810b07f25ce2..f666d873b7ff51 100644
--- a/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4
+++ b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4
@@ -22,107 +22,249 @@ options {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Statements
+/**
+ * Toplevel rule, entrypoint to the whole grammar
+ */
statement
- : ( selectStatement | updateStatement | deleteStatement | insertStatement ) EOF
+ : (selectStatement | updateStatement | deleteStatement | insertStatement) EOF
;
+/**
+ * A 'select' query
+ */
selectStatement
- : querySpec
+ : queryExpression
+ ;
+
+/**
+ * A 'select' query that occurs within another statement
+ */
+subquery
+ : queryExpression
;
+/**
+ * A declaration of a root entity, with an optional identification variable
+ */
+targetEntity
+ : entityName variable?
+ ;
+
+/**
+ * A 'delete' statement
+ */
deleteStatement
- : DELETE FROM? entityName identificationVariableDef? whereClause?
+ : DELETE FROM? targetEntity whereClause?
;
+/**
+ * An 'update' statement
+ */
updateStatement
- : UPDATE FROM? entityName identificationVariableDef? setClause whereClause?
+ : UPDATE VERSIONED? targetEntity setClause whereClause?
;
+/**
+ * An 'set' list of assignments in an 'update' statement
+ */
setClause
- : SET assignment+
+ : SET assignment (COMMA assignment)*
;
+/**
+ * An assignment to an entity attribute in an 'update' statement
+ */
assignment
- : dotIdentifierSequence EQUAL expression
+ : simplePath EQUAL expressionOrPredicate
;
+/**
+ * An 'insert' statement
+ */
insertStatement
-// todo (6.0 : VERSIONED
- : INSERT insertSpec querySpec
+ : INSERT INTO? targetEntity targetFields (queryExpression | valuesList)
;
-insertSpec
- : intoSpec targetFieldsSpec
+/**
+ * The list of target entity attributes in an 'insert' statement
+ */
+targetFields
+ : LEFT_PAREN simplePath (COMMA simplePath)* RIGHT_PAREN
;
-intoSpec
- : INTO entityName
+/**
+ * A 'values' clause in an 'insert' statement, with one of more tuples of values to insert
+ */
+valuesList
+ : VALUES values (COMMA values)*
;
-targetFieldsSpec
- :
- LEFT_PAREN dotIdentifierSequence (COMMA dotIdentifierSequence)* RIGHT_PAREN
+/**
+ * A tuple of values to insert in an 'insert' statement
+ */
+values
+ : LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)* RIGHT_PAREN
;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// QUERY SPEC - general structure of root sqm or sub sqm
-querySpec
- : selectClause? fromClause whereClause? ( groupByClause havingClause? )? orderByClause? limitClause? offsetClause?
+withClause
+ : WITH cte (COMMA cte)*
+ ;
+
+cte
+ : identifier AS (NOT? MATERIALIZED)? LEFT_PAREN queryExpression RIGHT_PAREN searchClause? cycleClause?
+ ;
+
+cteAttributes
+ : identifier (COMMA identifier)*
+ ;
+
+searchClause
+ : SEARCH (BREADTH|DEPTH) FIRST BY searchSpecifications SET identifier
+ ;
+
+searchSpecifications
+ : searchSpecification (COMMA searchSpecification)*
+ ;
+
+searchSpecification
+ : identifier sortDirection? nullsPrecedence?
+ ;
+
+cycleClause
+ : CYCLE cteAttributes SET identifier (TO literal DEFAULT literal)? (USING identifier)?
+ ;
+
+/**
+ * A toplevel query of subquery, which may be a union or intersection of subqueries
+ */
+queryExpression
+ : withClause? orderedQuery # SimpleQueryGroup
+ | withClause? orderedQuery (setOperator orderedQuery)+ # SetQueryGroup
+ ;
+
+/**
+ * A query with an optional 'order by' clause
+ */
+orderedQuery
+ : query queryOrder? # QuerySpecExpression
+ | LEFT_PAREN queryExpression RIGHT_PAREN queryOrder? # NestedQueryExpression
+ | queryOrder # QueryOrderExpression
+ ;
+
+/**
+ * An operator whose operands are whole queries
+ */
+setOperator
+ : UNION ALL?
+ | INTERSECT ALL?
+ | EXCEPT ALL?
+ ;
+
+/**
+ * The 'order by' clause and optional subclauses for limiting and pagination
+ */
+queryOrder
+ : orderByClause limitClause? offsetClause? fetchClause?
+ ;
+
+/**
+ * An unordered query, with just projection, restriction, and aggregation
+ *
+ * - The 'select' clause may come first, in which case 'from' is optional
+ * - The 'from' clause may come first, in which case 'select' is optional, and comes last
+ */
+query
+// TODO: add with clause
+ : selectClause fromClause? whereClause? (groupByClause havingClause?)?
+ | fromClause whereClause? (groupByClause havingClause?)? selectClause?
+ | whereClause
;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// FROM clause
+/**
+ * The 'from' clause of a query
+ */
fromClause
- : FROM fromClauseSpace (COMMA fromClauseSpace)*
+ : FROM entityWithJoins (COMMA entityWithJoins)*
;
-fromClauseSpace
- : pathRoot ( crossJoin | jpaCollectionJoin | qualifiedJoin )*
+/**
+ * The declaration of a root entity in 'from' clause, along with its joins
+ */
+entityWithJoins
+ : fromRoot (join | crossJoin | jpaCollectionJoin)*
;
-pathRoot
- : entityName (identificationVariableDef)?
+/**
+ * A root entity declaration in the 'from' clause, with optional identification variable
+ */
+fromRoot
+ : entityName variable? # RootEntity
+ | LEFT_PAREN subquery RIGHT_PAREN variable? # RootSubquery
;
/**
- * Rule for dotIdentifierSequence where we expect an entity-name. The extra
- * "rule layer" allows the walker to specially handle such a case (to use a special
- * org.hibernate.query.hql.DotIdentifierConsumer, etc)
+ * An entity name, for identifying the root entity
*/
entityName
- : dotIdentifierSequence
+ : identifier (DOT identifier)*
;
-identificationVariableDef
- : (AS identifier)
- | IDENTIFIER
+/**
+ * An identification variable (an entity alias)
+ */
+variable
+ : AS identifier
+ | nakedIdentifier
;
+/**
+ * A 'cross join' to a second root entity (a cartesian product)
+ */
crossJoin
- : CROSS JOIN pathRoot (identificationVariableDef)?
+ : CROSS JOIN entityName variable?
;
+/**
+ * Deprecated syntax dating back to EJB-QL prior to EJB 3, required by JPA, never documented in Hibernate
+ */
jpaCollectionJoin
- : COMMA IN LEFT_PAREN path RIGHT_PAREN (identificationVariableDef)?
+ : COMMA IN LEFT_PAREN path RIGHT_PAREN variable?
;
-qualifiedJoin
- : joinTypeQualifier JOIN FETCH? qualifiedJoinRhs (qualifiedJoinPredicate)?
+/**
+ * A 'join', with an optional 'on' or 'with' clause
+ */
+join
+ : joinType JOIN FETCH? joinTarget joinRestriction?
;
-joinTypeQualifier
+/**
+ * The inner or outer join type
+ */
+joinType
: INNER?
| (LEFT|RIGHT|FULL)? OUTER?
;
-qualifiedJoinRhs
- : path (identificationVariableDef)?
+/**
+ * The joined path, with an optional identification variable
+ */
+joinTarget
+ : path variable? #JoinPath
+ | LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable? #JoinSubquery
;
-qualifiedJoinPredicate
+/**
+ * An extra restriction added to the join condition
+ */
+joinRestriction
: (ON | WITH) predicate
;
@@ -131,472 +273,1064 @@ qualifiedJoinPredicate
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// SELECT clause
+/**
+ * The 'select' clause of a query
+ */
selectClause
- : SELECT DISTINCT? selectionList
+ : SELECT DISTINCT? selectionList
;
+/**
+ * A projection list: a list of selected items
+ */
selectionList
: selection (COMMA selection)*
;
+/**
+ * An element of a projection list: a selected item, with an optional alias
+ */
selection
- : selectExpression (resultIdentifier)?
+ : selectExpression variable?
;
+/**
+ * A selected item ocurring in the 'select' clause
+ */
selectExpression
- : dynamicInstantiation
- | jpaSelectObjectSyntax
- | mapEntrySelection
- | expression
- ;
-
-resultIdentifier
- : (AS identifier)
- | IDENTIFIER
+ : instantiation
+ | mapEntrySelection
+ | jpaSelectObjectSyntax
+ | expressionOrPredicate
;
+/**
+ * The special function entry() which may only occur in the 'select' clause
+ */
mapEntrySelection
: ENTRY LEFT_PAREN path RIGHT_PAREN
;
-dynamicInstantiation
- : NEW dynamicInstantiationTarget LEFT_PAREN dynamicInstantiationArgs RIGHT_PAREN
+/**
+ * Instantiation using 'select new'
+ */
+instantiation
+ : NEW instantiationTarget LEFT_PAREN instantiationArguments RIGHT_PAREN
;
-dynamicInstantiationTarget
+/**
+ * The type to be instantiated with 'select new', 'list', 'map', or a fuly-qualified Java class name
+ */
+instantiationTarget
: LIST
| MAP
- | dotIdentifierSequence
+ | simplePath
;
-dynamicInstantiationArgs
- : dynamicInstantiationArg ( COMMA dynamicInstantiationArg )*
+/**
+ * The arguments to a 'select new' instantiation
+ */
+instantiationArguments
+ : instantiationArgument (COMMA instantiationArgument)*
;
-dynamicInstantiationArg
- : dynamicInstantiationArgExpression (AS? identifier)?
+/**
+ * A single argument in a 'select new' instantiation, with an optional alias
+ */
+instantiationArgument
+ : instantiationArgumentExpression variable?
;
-dynamicInstantiationArgExpression
- : expression
- | dynamicInstantiation
+/**
+ * A single argument in a 'select new' instantiation: an expression, or a nested instantiation
+ */
+instantiationArgumentExpression
+ : expressionOrPredicate
+ | instantiation
;
+/**
+ * Deprecated syntax dating back to EJB-QL prior to EJB 3, required by JPA, never documented in Hibernate
+ */
jpaSelectObjectSyntax
- : OBJECT LEFT_PAREN identifier RIGHT_PAREN
+ : OBJECT LEFT_PAREN identifier RIGHT_PAREN
;
-
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Path structures
-dotIdentifierSequence
- : identifier dotIdentifierSequenceContinuation*
+/**
+ * A simple path expression
+ *
+ * - a reference to an identification variable (not case-sensitive),
+ * - followed by a list of period-separated identifiers (case-sensitive)
+ */
+simplePath
+ : identifier simplePathElement*
;
-dotIdentifierSequenceContinuation
+/**
+ * An element of a simple path expression: a period, and an identifier (case-sensitive)
+ */
+simplePathElement
: DOT identifier
;
-
/**
+ * A much more complicated path expression involving operators and functions
+ *
* A path which needs to be resolved semantically. This recognizes
* any path-like structure. Generally, the path is semantically
* interpreted by the consumer of the parse-tree. However, there
* are certain cases where we can syntactically recognize a navigable
- * path; see `syntacticNavigablePath` rule
+ * path; see 'syntacticNavigablePath' rule
*/
path
- : syntacticDomainPath (pathContinuation)?
+ : syntacticDomainPath pathContinuation?
| generalPathFragment
;
+/**
+ * A continuation of a path expression "broken" by an operator or function
+ */
pathContinuation
- : DOT dotIdentifierSequence (DOT pathContinuation)?
+ : DOT simplePath
;
/**
+ * An operator or function that may occur within a path expression
+ *
* Rule for cases where we syntactically know that the path is a
* "domain path" because it is one of these special cases:
*
* * TREAT( path )
* * ELEMENTS( path )
+ * * INDICES( path )
* * VALUE( path )
* * KEY( path )
* * path[ selector ]
*/
syntacticDomainPath
: treatedNavigablePath
- | collectionElementNavigablePath
+ | collectionValueNavigablePath
| mapKeyNavigablePath
- | dotIdentifierSequence indexedPathAccessFragment
+ | simplePath indexedPathAccessFragment
;
/**
- * The main path rule. Recognition for all normal path structures including
+ * The main path rule
+ *
+ * Recognition for all normal path structures including
* class, field and enum references as well as navigable paths.
*
* NOTE : this rule does *not* cover the special syntactic navigable path
* cases: TREAT, KEY, ELEMENTS, VALUES
*/
generalPathFragment
- : dotIdentifierSequence (indexedPathAccessFragment)?
+ : simplePath indexedPathAccessFragment?
;
+/**
+ * In index operator that "breaks" a path expression
+ */
indexedPathAccessFragment
: LEFT_BRACKET expression RIGHT_BRACKET (DOT generalPathFragment)?
;
+/**
+ * A 'treat()' function that "breaks" a path expression
+ */
treatedNavigablePath
- : TREAT LEFT_PAREN path AS dotIdentifierSequence RIGHT_PAREN (pathContinuation)?
+ : TREAT LEFT_PAREN path AS simplePath RIGHT_PAREN pathContinuation?
;
-collectionElementNavigablePath
- : (VALUE | ELEMENTS) LEFT_PAREN path RIGHT_PAREN (pathContinuation)?
+/**
+ * A 'value()' function that "breaks" a path expression
+ */
+collectionValueNavigablePath
+ : elementValueQuantifier LEFT_PAREN path RIGHT_PAREN pathContinuation?
;
+/**
+ * A 'key()' or 'index()' function that "breaks" a path expression
+ */
mapKeyNavigablePath
- : KEY LEFT_PAREN path RIGHT_PAREN (pathContinuation)?
+ : indexKeyQuantifier LEFT_PAREN path RIGHT_PAREN pathContinuation?
;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// GROUP BY clause
+/**
+ * The 'group by' clause of a query, controls aggregation
+ */
groupByClause
- : GROUP BY groupingSpecification
- ;
-
-groupingSpecification
- : groupingValue ( COMMA groupingValue )*
+ : GROUP BY groupByExpression (COMMA groupByExpression)*
;
-groupingValue
- : expression collationSpecification?
+/**
+ * A grouped item that occurs in the 'group by' clause
+ *
+ * a select item alias, an ordinal position of a select item, or an expression
+ */
+groupByExpression
+ : identifier
+ | INTEGER_LITERAL
+ | expression
;
-
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//HAVING clause
+/**
+ * The 'having' clause of a query, a restriction on the grouped data
+ */
havingClause
- : HAVING predicate
+ : HAVING predicate
;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ORDER BY clause
+/**
+ * The 'order by' clause of a query, controls sorting
+ */
orderByClause
-// todo (6.0) : null precedence
: ORDER BY sortSpecification (COMMA sortSpecification)*
;
+/**
+ * Specialized rule for ordered Map and Set '@OrderBy' handling
+ */
+orderByFragment
+ : sortSpecification (COMMA sortSpecification)*
+ ;
+
+/**
+ * A rule for sorting an item in the 'order by' clause
+ */
sortSpecification
- : expression collationSpecification? orderingSpecification?
+ : sortExpression sortDirection? nullsPrecedence?
;
-collationSpecification
- : COLLATE collateName
+/**
+ * A rule for sorting null values
+ */
+nullsPrecedence
+ : NULLS (FIRST | LAST)
+ ;
+
+/**
+ * A sorted item that occurs in the 'order by' clause
+ *
+ * a select item alias, an ordinal position of a select item, or an expression
+ */
+sortExpression
+ : identifier
+ | INTEGER_LITERAL
+ | expression
;
-collateName
- : dotIdentifierSequence
+/**
+ * The direction in which to sort
+ */
+sortDirection
+ : ASC
+ | DESC
+ ;
+
+/**
+ * The special 'collate()' functions
+ */
+collateFunction
+ : COLLATE LEFT_PAREN expression AS collation RIGHT_PAREN
;
-orderingSpecification
- : ASC
- | DESC
+/**
+ * The name of a database-defined collation
+ *
+ * Certain databases allow a period in a collation name
+ */
+collation
+ : simplePath
;
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// LIMIT/OFFSET clause
+/**
+ * A 'limit' on the number of query results
+ */
limitClause
- : LIMIT parameterOrNumberLiteral
+ : LIMIT parameterOrIntegerLiteral
;
+/**
+ * An 'offset' of the first query result to return
+ */
offsetClause
- : OFFSET parameterOrNumberLiteral
+ : OFFSET parameterOrIntegerLiteral
+ (ROW | ROWS)? // no semantics
+ ;
+
+/**
+ * A much more complex syntax for limits
+ */
+fetchClause
+ : FETCH
+ (FIRST | NEXT) // no semantics
+ fetchCountOrPercent
+ (ROW | ROWS) // no semantics
+ (ONLY | WITH TIES)
+ ;
+
+fetchCountOrPercent
+ : parameterOrIntegerLiteral
+ | parameterOrNumberLiteral PERCENT
+ ;
+
+/**
+ * An parameterizable integer literal
+ */
+parameterOrIntegerLiteral
+ : parameter
+ | INTEGER_LITERAL
;
+/**
+ * An parameterizable numeric literal
+ */
parameterOrNumberLiteral
: parameter
| INTEGER_LITERAL
+ | LONG_LITERAL
+ | FLOAT_LITERAL
+ | DOUBLE_LITERAL
;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// WHERE clause & Predicates
+/**
+ * The 'were' clause of a query, update statement, or delete statement
+ */
whereClause
- : WHERE predicate
+ : WHERE predicate
;
+/**
+ * A boolean-valued expression, usually used to express a restriction
+ */
predicate
- : LEFT_PAREN predicate RIGHT_PAREN # GroupedPredicate
- | predicate OR predicate # OrPredicate
- | predicate AND predicate # AndPredicate
- | NOT predicate # NegatedPredicate
- | expression IS (NOT)? NULL # IsNullPredicate
- | expression IS (NOT)? EMPTY # IsEmptyPredicate
- | expression EQUAL expression # EqualityPredicate
- | expression NOT_EQUAL expression # InequalityPredicate
- | expression GREATER expression # GreaterThanPredicate
- | expression GREATER_EQUAL expression # GreaterThanOrEqualPredicate
- | expression LESS expression # LessThanPredicate
- | expression LESS_EQUAL expression # LessThanOrEqualPredicate
- | expression (NOT)? IN inList # InPredicate
- | expression (NOT)? BETWEEN expression AND expression # BetweenPredicate
- | expression (NOT)? LIKE expression (likeEscape)? # LikePredicate
- | MEMBER OF path # MemberOfPredicate
+ //highest to lowest precedence
+ : LEFT_PAREN predicate RIGHT_PAREN # GroupedPredicate
+ | expression IS NOT? NULL # IsNullPredicate
+ | expression IS NOT? EMPTY # IsEmptyPredicate
+ | expression IS NOT? TRUE # IsTruePredicate
+ | expression IS NOT? FALSE # IsFalsePredicate
+ | expression IS NOT? DISTINCT FROM expression # IsDistinctFromPredicate
+ | expression NOT? MEMBER OF? path # MemberOfPredicate
+ | expression NOT? IN inList # InPredicate
+ | expression NOT? BETWEEN expression AND expression # BetweenPredicate
+ | expression NOT? (LIKE | ILIKE) expression likeEscape? # LikePredicate
+ | expression comparisonOperator expression # ComparisonPredicate
+ | EXISTS collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN # ExistsCollectionPartPredicate
+ | EXISTS expression # ExistsPredicate
+ | NOT predicate # NegatedPredicate
+ | predicate AND predicate # AndPredicate
+ | predicate OR predicate # OrPredicate
+ | expression # BooleanExpressionPredicate
+ ;
+
+/**
+ * An operator which compares values for equality or order
+ */
+comparisonOperator
+ : EQUAL
+ | NOT_EQUAL
+ | GREATER
+ | GREATER_EQUAL
+ | LESS
+ | LESS_EQUAL
;
+/**
+ * Any right operand of the 'in' operator
+ *
+ * A list of values, a parameter (for a parameterized list of values), a subquery, or an 'elements()' or 'indices()' function
+ */
inList
- : ELEMENTS? LEFT_PAREN dotIdentifierSequence RIGHT_PAREN # PersistentCollectionReferenceInList
- | LEFT_PAREN expression (COMMA expression)* RIGHT_PAREN # ExplicitTupleInList
- | expression # SubQueryInList
+ : collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN # PersistentCollectionReferenceInList
+ | LEFT_PAREN (expressionOrPredicate (COMMA expressionOrPredicate)*)? RIGHT_PAREN# ExplicitTupleInList
+ | LEFT_PAREN subquery RIGHT_PAREN # SubqueryInList
+ | parameter # ParamInList
;
+/**
+ * A single character used to escape the '_' and '%' wildcards in a 'like' pattern
+ */
likeEscape
- : ESCAPE expression
+ : ESCAPE (STRING_LITERAL | JAVA_STRING_LITERAL | parameter)
;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Expression
+/**
+ * An expression, excluding boolean expressions
+ */
expression
- : expression DOUBLE_PIPE expression # ConcatenationExpression
- | expression PLUS expression # AdditionExpression
- | expression MINUS expression # SubtractionExpression
- | expression ASTERISK expression # MultiplicationExpression
- | expression SLASH expression # DivisionExpression
- | expression PERCENT expression # ModuloExpression
- // todo (6.0) : should these unary plus/minus rules only apply to literals?
- // if so, move the MINUS / PLUS recognition to the `literal` rule
- // specificcally for numeric literals
- | MINUS expression # UnaryMinusExpression
- | PLUS expression # UnaryPlusExpression
- | caseStatement # CaseExpression
- | coalesce # CoalesceExpression
- | nullIf # NullIfExpression
- | literal # LiteralExpression
- | parameter # ParameterExpression
- | entityTypeReference # EntityTypeExpression
- | path # PathExpression
- | function # FunctionExpression
- | LEFT_PAREN querySpec RIGHT_PAREN # SubQueryExpression
+ //highest to lowest precedence
+ : LEFT_PAREN expression RIGHT_PAREN # GroupedExpression
+ | LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)+ RIGHT_PAREN # TupleExpression
+ | LEFT_PAREN subquery RIGHT_PAREN # SubqueryExpression
+ | primaryExpression # BarePrimaryExpression
+ | signOperator numericLiteral # UnaryNumericLiteralExpression
+ | signOperator expression # UnaryExpression
+ | expression datetimeField # ToDurationExpression
+ | expression BY datetimeField # FromDurationExpression
+ | expression multiplicativeOperator expression # MultiplicationExpression
+ | expression additiveOperator expression # AdditionExpression
+ | expression DOUBLE_PIPE expression # ConcatenationExpression
;
-entityTypeReference
- : TYPE LEFT_PAREN (path | parameter) RIGHT_PAREN
+/**
+ * An expression not involving operators
+ */
+primaryExpression
+ : caseList # CaseExpression
+ | literal # LiteralExpression
+ | parameter # ParameterExpression
+ | entityTypeReference # EntityTypeExpression
+ | entityIdReference # EntityIdExpression
+ | entityVersionReference # EntityVersionExpression
+ | entityNaturalIdReference # EntityNaturalIdExpression
+ | toOneFkReference # ToOneFkExpression
+ | syntacticDomainPath pathContinuation? # SyntacticPathExpression
+ | function # FunctionExpression
+ | generalPathFragment # GeneralPathExpression
;
-caseStatement
- : simpleCaseStatement
- | searchedCaseStatement
+/**
+ * Any expression, including boolean expressions
+ */
+expressionOrPredicate
+ : expression
+ | predicate
;
-simpleCaseStatement
- : CASE expression (simpleCaseWhen)+ (caseOtherwise)? END
+collectionQuantifier
+ : elementsValuesQuantifier
+ | indicesKeysQuantifier
;
-simpleCaseWhen
- : WHEN expression THEN expression
+elementValueQuantifier
+ : ELEMENT
+ | VALUE
;
-caseOtherwise
- : ELSE expression
+indexKeyQuantifier
+ : INDEX
+ | KEY
;
-searchedCaseStatement
- : CASE (searchedCaseWhen)+ (caseOtherwise)? END
+elementsValuesQuantifier
+ : ELEMENTS
+ | VALUES
;
-searchedCaseWhen
- : WHEN predicate THEN expression
+indicesKeysQuantifier
+ : INDICES
+ | KEYS
;
-coalesce
- : COALESCE LEFT_PAREN expression (COMMA expression)+ RIGHT_PAREN
+/**
+ * A binary operator with the same precedence as *
+ */
+multiplicativeOperator
+ : SLASH
+ | PERCENT_OP
+ | ASTERISK
;
-nullIf
- : NULLIF LEFT_PAREN expression COMMA expression RIGHT_PAREN
+/**
+ * A binary operator with the same precedence as +
+ */
+additiveOperator
+ : PLUS
+ | MINUS
;
-literal
- : STRING_LITERAL
- | CHARACTER_LITERAL
- | INTEGER_LITERAL
- | LONG_LITERAL
- | BIG_INTEGER_LITERAL
- | FLOAT_LITERAL
- | DOUBLE_LITERAL
- | BIG_DECIMAL_LITERAL
- | HEX_LITERAL
- | OCTAL_LITERAL
- | NULL
- | TRUE
- | FALSE
- | timestampLiteral
- | dateLiteral
- | timeLiteral
+/**
+ * A unary prefix operator
+ */
+signOperator
+ : PLUS
+ | MINUS
;
-// todo (6.0) : expand temporal literal support to Java 8 temporal types
-// * Instant -> {instant '...'}
-// * LocalDate -> {localDate '...'}
-// * LocalDateTime -> {localDateTime '...'}
-// * OffsetDateTime -> {offsetDateTime '...'}
-// * OffsetTime -> {offsetTime '...'}
-// * ZonedDateTime -> {localDate '...'}
-// * ...
-//
-// Few things:
-// 1) the markers above are just initial thoughts. They are obviously verbose. Maybe acronyms or shortened forms would be better
-// 2) we may want to stay away from all of the timezone headaches by not supporting local, zoned and offset forms
-
-timestampLiteral
- : TIMESTAMP_ESCAPE_START dateTimeLiteralText RIGHT_BRACE
+/**
+ * The special function 'type()'
+ */
+entityTypeReference
+ : TYPE LEFT_PAREN (path | parameter) RIGHT_PAREN
;
-dateLiteral
- : DATE_ESCAPE_START dateTimeLiteralText RIGHT_BRACE
+/**
+ * The special function 'id()'
+ */
+entityIdReference
+ : ID LEFT_PAREN path RIGHT_PAREN pathContinuation?
;
-timeLiteral
- : TIME_ESCAPE_START dateTimeLiteralText RIGHT_BRACE
+/**
+ * The special function 'version()'
+ */
+entityVersionReference
+ : VERSION LEFT_PAREN path RIGHT_PAREN
;
-dateTimeLiteralText
- : STRING_LITERAL | CHARACTER_LITERAL
+/**
+ * The special function 'naturalid()'
+ */
+entityNaturalIdReference
+ : NATURALID LEFT_PAREN path RIGHT_PAREN pathContinuation?
;
-parameter
- : COLON identifier # NamedParameter
- | QUESTION_MARK INTEGER_LITERAL? # PositionalParameter
+/**
+ * The special function 'fk()'
+ */
+toOneFkReference
+ : FK LEFT_PAREN path RIGHT_PAREN
;
-function
- : standardFunction
- | aggregateFunction
- | jpaCollectionFunction
- | hqlCollectionFunction
- | jpaNonStandardFunction
- | nonStandardFunction
+/**
+ * A 'case' expression, which comes in two forms: "simple", and "searched"
+ */
+caseList
+ : simpleCaseList
+ | searchedCaseList
;
-jpaNonStandardFunction
- : FUNCTION LEFT_PAREN jpaNonStandardFunctionName (COMMA nonStandardFunctionArguments)? RIGHT_PAREN
+/**
+ * A simple 'case' expression
+ */
+simpleCaseList
+ : CASE expressionOrPredicate simpleCaseWhen+ caseOtherwise? END
;
-jpaNonStandardFunctionName
- : STRING_LITERAL
+/**
+ * The 'when' clause of a simple case
+ */
+simpleCaseWhen
+ : WHEN expression THEN expressionOrPredicate
+ ;
+
+/**
+ * The 'else' clause of a 'case' expression
+ */
+caseOtherwise
+ : ELSE expressionOrPredicate
+ ;
+
+/**
+ * A searched 'case' expression
+ */
+searchedCaseList
+ : CASE searchedCaseWhen+ caseOtherwise? END
+ ;
+
+/**
+ * The 'when' clause of a searched case
+ */
+searchedCaseWhen
+ : WHEN predicate THEN expressionOrPredicate
+ ;
+
+/**
+ * A literal value
+ */
+literal
+ : STRING_LITERAL
+ | JAVA_STRING_LITERAL
+ | NULL
+ | booleanLiteral
+ | numericLiteral
+ | binaryLiteral
+ | temporalLiteral
+ | generalizedLiteral
+ ;
+
+/**
+ * A boolean literal value
+ */
+booleanLiteral
+ : TRUE
+ | FALSE
+ ;
+
+/**
+ * A numeric literal value, including hexadecimal literals
+ */
+numericLiteral
+ : INTEGER_LITERAL
+ | LONG_LITERAL
+ | BIG_INTEGER_LITERAL
+ | FLOAT_LITERAL
+ | DOUBLE_LITERAL
+ | BIG_DECIMAL_LITERAL
+ | HEX_LITERAL
+ ;
+
+/**
+ * A binary literal value, as a SQL-style literal, or a braced list of byte literals
+ */
+binaryLiteral
+ : BINARY_LITERAL
+ | LEFT_BRACE HEX_LITERAL (COMMA HEX_LITERAL)* RIGHT_BRACE
;
-nonStandardFunction
- : nonStandardFunctionName LEFT_PAREN nonStandardFunctionArguments? RIGHT_PAREN
+/**
+ * A literal date, time, or datetime, in HQL syntax, or as a JDBC-style "escape" syntax
+ */
+temporalLiteral
+ : dateTimeLiteral
+ | dateLiteral
+ | timeLiteral
+ | jdbcTimestampLiteral
+ | jdbcDateLiteral
+ | jdbcTimeLiteral
;
-nonStandardFunctionName
- : dotIdentifierSequence
+/**
+ * A literal datetime, in braces, or with the 'datetime' keyword
+ */
+dateTimeLiteral
+ : localDateTimeLiteral
+ | zonedDateTimeLiteral
+ | offsetDateTimeLiteral
;
-nonStandardFunctionArguments
- : expression (COMMA expression)*
+localDateTimeLiteral
+ : LEFT_BRACE localDateTime RIGHT_BRACE
+ | LOCAL? DATETIME localDateTime
;
-jpaCollectionFunction
- : SIZE LEFT_PAREN path RIGHT_PAREN # CollectionSizeFunction
- | INDEX LEFT_PAREN identifier RIGHT_PAREN # CollectionIndexFunction
+zonedDateTimeLiteral
+ : LEFT_BRACE zonedDateTime RIGHT_BRACE
+ | ZONED? DATETIME zonedDateTime
;
-hqlCollectionFunction
- : MAXINDEX LEFT_PAREN path RIGHT_PAREN # MaxIndexFunction
- | MAXELEMENT LEFT_PAREN path RIGHT_PAREN # MaxElementFunction
- | MININDEX LEFT_PAREN path RIGHT_PAREN # MinIndexFunction
- | MINELEMENT LEFT_PAREN path RIGHT_PAREN # MinElementFunction
+offsetDateTimeLiteral
+ : LEFT_BRACE offsetDateTime RIGHT_BRACE
+ | OFFSET? DATETIME offsetDateTimeWithMinutes
+ ;
+
+/**
+ * A literal date, in braces, or with the 'date' keyword
+ */
+dateLiteral
+ : LEFT_BRACE date RIGHT_BRACE
+ | LOCAL? DATE date
+ ;
+
+/**
+ * A literal time, in braces, or with the 'time' keyword
+ */
+timeLiteral
+ : LEFT_BRACE time RIGHT_BRACE
+ | LOCAL? TIME time
+ ;
+
+/**
+ * A literal datetime
+ */
+ dateTime
+ : localDateTime
+ | zonedDateTime
+ | offsetDateTime
+ ;
+
+localDateTime
+ : date time
+ ;
+
+zonedDateTime
+ : date time zoneId
+ ;
+
+offsetDateTime
+ : date time offset
+ ;
+
+offsetDateTimeWithMinutes
+ : date time offsetWithMinutes
+ ;
+
+/**
+ * A literal date
+ */
+date
+ : year MINUS month MINUS day
+ ;
+
+/**
+ * A literal time
+ */
+time
+ : hour COLON minute (COLON second)?
+ ;
+
+/**
+ * A literal offset
+ */
+offset
+ : (PLUS | MINUS) hour (COLON minute)?
;
+offsetWithMinutes
+ : (PLUS | MINUS) hour COLON minute
+ ;
+
+year: INTEGER_LITERAL;
+month: INTEGER_LITERAL;
+day: INTEGER_LITERAL;
+hour: INTEGER_LITERAL;
+minute: INTEGER_LITERAL;
+second: INTEGER_LITERAL | DOUBLE_LITERAL;
+zoneId
+ : IDENTIFIER (SLASH IDENTIFIER)?
+ | STRING_LITERAL;
+
+/**
+ * A JDBC-style timestamp escape, as required by JPQL
+ */
+jdbcTimestampLiteral
+ : TIMESTAMP_ESCAPE_START (dateTime | genericTemporalLiteralText) RIGHT_BRACE
+ ;
+
+/**
+ * A JDBC-style date escape, as required by JPQL
+ */
+jdbcDateLiteral
+ : DATE_ESCAPE_START (date | genericTemporalLiteralText) RIGHT_BRACE
+ ;
+
+/**
+ * A JDBC-style time escape, as required by JPQL
+ */
+jdbcTimeLiteral
+ : TIME_ESCAPE_START (time | genericTemporalLiteralText) RIGHT_BRACE
+ ;
+
+genericTemporalLiteralText
+ : STRING_LITERAL
+ ;
+
+/**
+ * A generic format for specifying literal values of arbitary types
+ */
+generalizedLiteral
+ : LEFT_BRACE generalizedLiteralType COLON generalizedLiteralText RIGHT_BRACE
+ ;
+
+generalizedLiteralType : STRING_LITERAL;
+generalizedLiteralText : STRING_LITERAL;
+
+
+/**
+ * A query parameter: a named parameter, or an ordinal parameter
+ */
+parameter
+ : COLON identifier # NamedParameter
+ | QUESTION_MARK INTEGER_LITERAL? # PositionalParameter
+ ;
+
+/**
+ * A function invocation that may occur in an arbitrary expression
+ */
+function
+ : standardFunction
+ | aggregateFunction
+ | collectionSizeFunction
+ | collectionAggregateFunction
+ | collectionFunctionMisuse
+ | jpaNonstandardFunction
+ | genericFunction
+ ;
+
+/**
+ * A syntax for calling user-defined or native database functions, required by JPQL
+ */
+jpaNonstandardFunction
+ : FUNCTION LEFT_PAREN jpaNonstandardFunctionName (COMMA genericFunctionArguments)? RIGHT_PAREN
+ ;
+
+/**
+ * The name of a user-defined or native database function, given as a quoted string
+ */
+jpaNonstandardFunctionName
+ : STRING_LITERAL
+ ;
+
+/**
+ * Any function invocation that follows the regular syntax
+ *
+ * The function name, followed by a parenthesized list of comma-separated expressions
+ */
+genericFunction
+ : genericFunctionName LEFT_PAREN (genericFunctionArguments | ASTERISK)? RIGHT_PAREN
+ nthSideClause? nullsClause? withinGroupClause? filterClause? overClause?
+ ;
+
+/**
+ * The name of a generic function, which may contain periods and quoted identifiers
+ *
+ * Names of generic functions are resolved against the SqmFunctionRegistry
+ */
+genericFunctionName
+ : simplePath
+ ;
+
+/**
+ * The arguments of a generic function
+ */
+genericFunctionArguments
+ : (DISTINCT | datetimeField COMMA)? expressionOrPredicate (COMMA expressionOrPredicate)*
+ ;
+
+/**
+ * The special 'size()' function defined by JPQL
+ */
+collectionSizeFunction
+ : SIZE LEFT_PAREN path RIGHT_PAREN
+ ;
+
+/**
+ * Special rule for 'max(elements())`, 'avg(keys())', 'sum(indices())`, etc., as defined by HQL
+ * Also the deprecated 'maxindex()', 'maxelement()', 'minindex()', 'minelement()' functions from old HQL
+ */
+collectionAggregateFunction
+ : (MAX|MIN|SUM|AVG) LEFT_PAREN elementsValuesQuantifier LEFT_PAREN path RIGHT_PAREN RIGHT_PAREN # ElementAggregateFunction
+ | (MAX|MIN|SUM|AVG) LEFT_PAREN indicesKeysQuantifier LEFT_PAREN path RIGHT_PAREN RIGHT_PAREN # IndexAggregateFunction
+ | (MAXELEMENT|MINELEMENT) LEFT_PAREN path RIGHT_PAREN # ElementAggregateFunction
+ | (MAXINDEX|MININDEX) LEFT_PAREN path RIGHT_PAREN # IndexAggregateFunction
+ ;
+
+/**
+ * To accommodate the misuse of elements() and indices() in the select clause
+ *
+ * (At some stage in the history of HQL, someone mixed them up with value() and index(),
+ * and so we have tests that insist they're interchangeable. Ugh.)
+ */
+collectionFunctionMisuse
+ : elementsValuesQuantifier LEFT_PAREN path RIGHT_PAREN
+ | indicesKeysQuantifier LEFT_PAREN path RIGHT_PAREN
+ ;
+
+/**
+ * The special 'every()', 'all()', 'any()' and 'some()' functions defined by HQL
+ *
+ * May be applied to a subquery or collection reference, or may occur as an aggregate function in the 'select' clause
+ */
aggregateFunction
- : avgFunction
- | sumFunction
- | minFunction
- | maxFunction
- | countFunction
+ : everyFunction
+ | anyFunction
+ | listaggFunction
;
-avgFunction
- : AVG LEFT_PAREN DISTINCT? expression RIGHT_PAREN
+/**
+ * The functions 'every()' and 'all()' are synonyms
+ */
+everyFunction
+ : everyAllQuantifier LEFT_PAREN predicate RIGHT_PAREN filterClause? overClause?
+ | everyAllQuantifier LEFT_PAREN subquery RIGHT_PAREN
+ | everyAllQuantifier collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN
;
-sumFunction
- : SUM LEFT_PAREN DISTINCT? expression RIGHT_PAREN
+/**
+ * The functions 'any()' and 'some()' are synonyms
+ */
+anyFunction
+ : anySomeQuantifier LEFT_PAREN predicate RIGHT_PAREN filterClause? overClause?
+ | anySomeQuantifier LEFT_PAREN subquery RIGHT_PAREN
+ | anySomeQuantifier collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN
;
-minFunction
- : MIN LEFT_PAREN DISTINCT? expression RIGHT_PAREN
+everyAllQuantifier
+ : EVERY
+ | ALL
+ ;
+
+anySomeQuantifier
+ : ANY
+ | SOME
+ ;
+
+/**
+ * The 'listagg()' ordered set-aggregate function
+ */
+listaggFunction
+ : LISTAGG LEFT_PAREN DISTINCT? expressionOrPredicate COMMA expressionOrPredicate onOverflowClause? RIGHT_PAREN
+ withinGroupClause? filterClause? overClause?
;
-maxFunction
- : MAX LEFT_PAREN DISTINCT? expression RIGHT_PAREN
+/**
+ * A 'on overflow' clause: what to do when the text data type used for 'listagg' overflows
+ */
+onOverflowClause
+ : ON OVERFLOW (ERROR | TRUNCATE expression? (WITH|WITHOUT) COUNT)
;
-countFunction
- : COUNT LEFT_PAREN DISTINCT? (expression | ASTERISK) RIGHT_PAREN
+/**
+ * A 'within group' clause: defines the order in which the ordered set-aggregate function should work
+ */
+withinGroupClause
+ : WITHIN GROUP LEFT_PAREN orderByClause RIGHT_PAREN
;
-standardFunction
- : castFunction
- | concatFunction
- | substringFunction
- | trimFunction
- | upperFunction
- | lowerFunction
- | lengthFunction
- | locateFunction
- | absFunction
- | sqrtFunction
- | modFunction
- | strFunction
- | currentDateFunction
- | currentTimeFunction
- | currentTimestampFunction
- | extractFunction
- | positionFunction
- | charLengthFunction
- | octetLengthFunction
- | bitLengthFunction
+/**
+ * A 'filter' clause: a restriction applied to an aggregate function
+ */
+filterClause
+ : FILTER LEFT_PAREN whereClause RIGHT_PAREN
+ ;
+
+/**
+ * A `nulls` clause: what should a value access window function do when encountering a `null`
+ */
+nullsClause
+ : RESPECT NULLS
+ | IGNORE NULLS
;
+/**
+ * A `nulls` clause: what should a value access window function do when encountering a `null`
+ */
+nthSideClause
+ : FROM FIRST
+ | FROM LAST
+ ;
+/**
+ * A 'over' clause: the specification of a window within which the function should act
+ */
+overClause
+ : OVER LEFT_PAREN partitionClause? orderByClause? frameClause? RIGHT_PAREN
+ ;
+
+/**
+ * A 'partition' clause: the specification the group within which a function should act in a window
+ */
+partitionClause
+ : PARTITION BY expression (COMMA expression)*
+ ;
+
+/**
+ * A 'frame' clause: the specification the content of the window
+ */
+frameClause
+ : (RANGE|ROWS|GROUPS) frameStart frameExclusion?
+ | (RANGE|ROWS|GROUPS) BETWEEN frameStart AND frameEnd frameExclusion?
+ ;
+
+/**
+ * The start of the window content
+ */
+frameStart
+ : CURRENT ROW
+ | UNBOUNDED PRECEDING
+ | expression PRECEDING
+ | expression FOLLOWING
+ ;
+
+/**
+ * The end of the window content
+ */
+frameEnd
+ : CURRENT ROW
+ | UNBOUNDED FOLLOWING
+ | expression PRECEDING
+ | expression FOLLOWING
+ ;
+
+/**
+ * A 'exclusion' clause: the specification what to exclude from the window content
+ */
+frameExclusion
+ : EXCLUDE CURRENT ROW
+ | EXCLUDE GROUP
+ | EXCLUDE TIES
+ | EXCLUDE NO OTHERS
+ ;
+
+/**
+ * Any function with an irregular syntax for the argument list
+ *
+ * These are all inspired by the syntax of ANSI SQL
+ */
+standardFunction
+ : castFunction
+ | extractFunction
+ | truncFunction
+ | formatFunction
+ | collateFunction
+ | substringFunction
+ | overlayFunction
+ | trimFunction
+ | padFunction
+ | positionFunction
+ | currentDateFunction
+ | currentTimeFunction
+ | currentTimestampFunction
+ | instantFunction
+ | localDateFunction
+ | localTimeFunction
+ | localDateTimeFunction
+ | offsetDateTimeFunction
+ | cube
+ | rollup
+ ;
+
+/**
+ * The 'cast()' function for typecasting
+ */
castFunction
: CAST LEFT_PAREN expression AS castTarget RIGHT_PAREN
;
+/**
+ * The target type for a typecast: a typename, together with length or precision/scale
+ */
castTarget
- // todo (6.0) : should allow either
- // - named cast (IDENTIFIER)
- // - JavaTypeDescriptorRegistry (imported) key
- // - java.sql.Types field NAME (alias for its value as a coded cast)
- // - "pass through"
- // - coded cast (INTEGER_LITERAL)
- // - SqlTypeDescriptorRegistry key
- : IDENTIFIER
+ : castTargetType (LEFT_PAREN INTEGER_LITERAL (COMMA INTEGER_LITERAL)? RIGHT_PAREN)?
;
-concatFunction
- : CONCAT LEFT_PAREN expression (COMMA expression)+ RIGHT_PAREN
+/**
+ * The name of the target type in a typecast
+ *
+ * Like the 'entityName' rule, we have a specialized dotIdentifierSequence rule
+ */
+castTargetType
+ returns [String fullTargetName]
+ : (i=identifier { $fullTargetName = _localctx.i.getText(); }) (DOT c=identifier { $fullTargetName += ("." + _localctx.c.getText() ); })*
;
+/**
+ * The two formats for the 'substring() function: one defined by JPQL, the other by ANSI SQL
+ */
substringFunction
- : (SUBSTRING | SUBSTR) LEFT_PAREN expression COMMA substringFunctionStartArgument (COMMA substringFunctionLengthArgument)? RIGHT_PAREN
+ : SUBSTRING LEFT_PAREN expression COMMA substringFunctionStartArgument (COMMA substringFunctionLengthArgument)? RIGHT_PAREN
+ | SUBSTRING LEFT_PAREN expression FROM substringFunctionStartArgument (FOR substringFunctionLengthArgument)? RIGHT_PAREN
;
substringFunctionStartArgument
@@ -607,6 +1341,9 @@ substringFunctionLengthArgument
: expression
;
+/**
+ * The ANSI SQL-style 'trim()' function
+ */
trimFunction
: TRIM LEFT_PAREN trimSpecification? trimCharacter? FROM? expression RIGHT_PAREN
;
@@ -618,205 +1355,414 @@ trimSpecification
;
trimCharacter
- : CHARACTER_LITERAL | STRING_LITERAL
- ;
-
-upperFunction
- : UPPER LEFT_PAREN expression RIGHT_PAREN
+ : STRING_LITERAL
;
-lowerFunction
- : LOWER LEFT_PAREN expression RIGHT_PAREN
+/**
+ * A 'pad()' function inspired by 'trim()'
+ */
+padFunction
+ : PAD LEFT_PAREN expression WITH padLength padSpecification padCharacter? RIGHT_PAREN
;
-lengthFunction
- : LENGTH LEFT_PAREN expression RIGHT_PAREN
+padSpecification
+ : LEADING
+ | TRAILING
;
-locateFunction
- : LOCATE LEFT_PAREN locateFunctionSubstrArgument COMMA locateFunctionStringArgument (COMMA locateFunctionStartArgument)? RIGHT_PAREN
+padCharacter
+ : STRING_LITERAL
;
-locateFunctionSubstrArgument
+padLength
: expression
;
-locateFunctionStringArgument
- : expression
+/**
+ * The ANSI SQL-style 'overlay()' function
+ */
+overlayFunction
+ : OVERLAY LEFT_PAREN overlayFunctionStringArgument PLACING overlayFunctionReplacementArgument FROM overlayFunctionStartArgument (FOR overlayFunctionLengthArgument)? RIGHT_PAREN
;
-locateFunctionStartArgument
+overlayFunctionStringArgument
: expression
;
-absFunction
- : ABS LEFT_PAREN expression RIGHT_PAREN
- ;
-
-sqrtFunction
- : SQRT LEFT_PAREN expression RIGHT_PAREN
- ;
-
-modFunction
- : MOD LEFT_PAREN modDividendArgument COMMA modDivisorArgument RIGHT_PAREN
+overlayFunctionReplacementArgument
+ : expression
;
-strFunction
- : STR LEFT_PAREN expression RIGHT_PAREN
- ;
-
-modDividendArgument
+overlayFunctionStartArgument
: expression
;
-modDivisorArgument
+overlayFunctionLengthArgument
: expression
;
+/**
+ * The deprecated current_date function required by JPQL
+ */
currentDateFunction
: CURRENT_DATE (LEFT_PAREN RIGHT_PAREN)?
+ | CURRENT DATE
;
+/**
+ * The deprecated current_time function required by JPQL
+ */
currentTimeFunction
: CURRENT_TIME (LEFT_PAREN RIGHT_PAREN)?
+ | CURRENT TIME
;
+/**
+ * The deprecated current_timestamp function required by JPQL
+ */
currentTimestampFunction
: CURRENT_TIMESTAMP (LEFT_PAREN RIGHT_PAREN)?
+ | CURRENT TIMESTAMP
+ ;
+
+/**
+ * The instant function, and deprecated current_instant function
+ */
+instantFunction
+ : CURRENT_INSTANT (LEFT_PAREN RIGHT_PAREN)? //deprecated legacy syntax
+ | INSTANT
+ ;
+
+/**
+ * The 'local datetime' function (or literal if you prefer)
+ */
+localDateTimeFunction
+ : LOCAL_DATETIME (LEFT_PAREN RIGHT_PAREN)?
+ | LOCAL DATETIME
+ ;
+
+/**
+ * The 'offset datetime' function (or literal if you prefer)
+ */
+offsetDateTimeFunction
+ : OFFSET_DATETIME (LEFT_PAREN RIGHT_PAREN)?
+ | OFFSET DATETIME
+ ;
+
+/**
+ * The 'local date' function (or literal if you prefer)
+ */
+localDateFunction
+ : LOCAL_DATE (LEFT_PAREN RIGHT_PAREN)?
+ | LOCAL DATE
;
+/**
+ * The 'local time' function (or literal if you prefer)
+ */
+localTimeFunction
+ : LOCAL_TIME (LEFT_PAREN RIGHT_PAREN)?
+ | LOCAL TIME
+ ;
+
+/**
+ * The 'format()' function for formatting dates and times according to a pattern
+ */
+formatFunction
+ : FORMAT LEFT_PAREN expression AS format RIGHT_PAREN
+ ;
+
+/**
+ * A format pattern, with a syntax inspired by by java.time.format.DateTimeFormatter
+ *
+ * see 'Dialect.appendDatetimeFormat()'
+ */
+format
+ : STRING_LITERAL
+ ;
+
+/**
+ * The 'extract()' function for extracting fields of dates, times, and datetimes
+ */
extractFunction
: EXTRACT LEFT_PAREN extractField FROM expression RIGHT_PAREN
+ | datetimeField LEFT_PAREN expression RIGHT_PAREN
+ ;
+
+/**
+ * The 'trunc()' function for truncating both numeric and datetime values
+ */
+truncFunction
+ : (TRUNC | TRUNCATE) LEFT_PAREN expression (COMMA (datetimeField | expression))? RIGHT_PAREN
;
+/**
+ * A field that may be extracted from a date, time, or datetime
+ */
extractField
: datetimeField
+ | dayField
+ | weekField
| timeZoneField
+ | dateOrTimeField
;
datetimeField
- : nonSecondDatetimeField
- | SECOND
- ;
-
-nonSecondDatetimeField
: YEAR
| MONTH
| DAY
+ | WEEK
+ | QUARTER
| HOUR
| MINUTE
+ | SECOND
+ | NANOSECOND
+ | EPOCH
+ ;
+
+dayField
+ : DAY OF MONTH
+ | DAY OF WEEK
+ | DAY OF YEAR
+ ;
+
+weekField
+ : WEEK OF MONTH
+ | WEEK OF YEAR
;
timeZoneField
- : TIMEZONE_HOUR
- | TIMEZONE_MINUTE
+ : OFFSET (HOUR | MINUTE)?
+ | TIMEZONE_HOUR | TIMEZONE_MINUTE
;
-positionFunction
- : POSITION LEFT_PAREN positionSubstrArgument IN positionStringArgument RIGHT_PAREN
+dateOrTimeField
+ : DATE
+ | TIME
;
-positionSubstrArgument
- : expression
+/**
+ * The ANSI SQL-style 'position()' function
+ */
+positionFunction
+ : POSITION LEFT_PAREN positionFunctionPatternArgument IN positionFunctionStringArgument RIGHT_PAREN
;
-positionStringArgument
+positionFunctionPatternArgument
: expression
;
-charLengthFunction
- : CAST LEFT_PAREN expression RIGHT_PAREN
+positionFunctionStringArgument
+ : expression
;
-octetLengthFunction
- : OCTET_LENGTH LEFT_PAREN expression RIGHT_PAREN
+/**
+ * The 'cube()' function specific to the 'group by' clause
+ */
+cube
+ : CUBE LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)* RIGHT_PAREN
;
-bitLengthFunction
- : BIT_LENGTH LEFT_PAREN expression RIGHT_PAREN
+/**
+ * The 'rollup()' function specific to the 'group by' clause
+ */
+rollup
+ : ROLLUP LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)* RIGHT_PAREN
;
/**
- * The `identifier` is used to provide "keyword as identifier" handling.
+ * Support for "soft" keywords which may be used as identifiers
+ *
+ * The 'identifier' rule is used to provide "keyword as identifier" handling.
*
* The lexer hands us recognized keywords using their specific tokens. This is important
* for the recognition of sqm structure, especially in terms of performance!
*
- * However we want to continue to allow users to use mopst keywords as identifiers (e.g., attribute names).
+ * However we want to continue to allow users to use most keywords as identifiers (e.g., attribute names).
* This parser rule helps with that. Here we expect that the caller already understands their
* context enough to know that keywords-as-identifiers are allowed.
*/
-identifier
+ // All except the possible optional following keywords LEFT, RIGHT, INNER, FULL, OUTER
+ nakedIdentifier
: IDENTIFIER
- | (ABS
- | ALL
+ | QUOTED_IDENTIFIER
+ | (ALL
| AND
| ANY
| AS
| ASC
| AVG
- | BY
| BETWEEN
- | BIT_LENGTH
| BOTH
+ | BREADTH
+ | BY
+ | CASE
| CAST
- | COALESCE
| COLLATE
- | CONCAT
| COUNT
| CROSS
+ | CUBE
+ | CURRENT
+ | CURRENT_DATE
+ | CURRENT_INSTANT
+ | CURRENT_TIME
+ | CURRENT_TIMESTAMP
+ | CYCLE
+ | DATE
+ | DATETIME
| DAY
+ | DEFAULT
| DELETE
+ | DEPTH
| DESC
| DISTINCT
+ | ELEMENT
| ELEMENTS
+ | ELSE
+ | EMPTY
+ | END
| ENTRY
+ | EPOCH
+ | ERROR
+ | ESCAPE
+ | EVERY
+ | EXCEPT
+ | EXCLUDE
+ | EXISTS
+ | EXTRACT
+ | FETCH
+ | FILTER
+ | FIRST
+ | FOLLOWING
+ | FOR
+ | FORMAT
| FROM
- | FULL
+// | FULL
| FUNCTION
| GROUP
+ | GROUPS
+ | HAVING
| HOUR
+ | ID
+ | IGNORE
+ | ILIKE
| IN
| INDEX
- | INNER
+ | INDICES
+// | INNER
| INSERT
+ | INSTANT
+ | INTERSECT
+ | INTO
+ | IS
| JOIN
| KEY
+ | KEYS
+ | LAST
+ | LATERAL
| LEADING
- | LEFT
- | LENGTH
+// | LEFT
| LIKE
+ | LIMIT
| LIST
- | LOWER
+ | LISTAGG
+ | LOCAL
+ | LOCAL_DATE
+ | LOCAL_DATETIME
+ | LOCAL_TIME
| MAP
+ | MATERIALIZED
| MAX
+ | MAXELEMENT
+ | MAXINDEX
+ | MEMBER
+ | MICROSECOND
+ | MILLISECOND
| MIN
+ | MINELEMENT
+ | MININDEX
| MINUTE
- | MEMBER
| MONTH
+ | NANOSECOND
+ | NATURALID
+ | NEW
+ | NEXT
+ | NO
+ | NOT
+ | NULLS
| OBJECT
+ | OF
+ | OFFSET
+ | OFFSET_DATETIME
| ON
+ | ONLY
| OR
| ORDER
- | OUTER
+ | OTHERS
+// | OUTER
+ | OVER
+ | OVERFLOW
+ | OVERLAY
+ | PAD
+ | PARTITION
+ | PERCENT
+ | PLACING
| POSITION
- | RIGHT
- | SELECT
+ | PRECEDING
+ | QUARTER
+ | RANGE
+ | RESPECT
+// | RIGHT
+ | ROLLUP
+ | ROW
+ | ROWS
+ | SEARCH
| SECOND
+ | SELECT
| SET
- | SQRT
- | STR
+ | SIZE
+ | SOME
| SUBSTRING
| SUM
+ | THEN
+ | TIES
+ | TIME
+ | TIMESTAMP
+ | TIMEZONE_HOUR
+ | TIMEZONE_MINUTE
+ | TO
| TRAILING
| TREAT
+ | TRIM
+ | TRUNC
+ | TRUNCATE
+ | TYPE
+ | UNBOUNDED
+ | UNION
| UPDATE
- | UPPER
+ | USING
| VALUE
+ | VALUES
+ | VERSION
+ | VERSIONED
+ | WEEK
+ | WHEN
| WHERE
| WITH
- | YEAR) {
- logUseOfReservedWordAsIdentifier(getCurrentToken());
+ | WITHIN
+ | WITHOUT
+ | YEAR
+ | ZONED) {
+ logUseOfReservedWordAsIdentifier( getCurrentToken() );
+ }
+ ;
+identifier
+ : nakedIdentifier
+ | (FULL
+ | INNER
+ | LEFT
+ | OUTER
+ | RIGHT) {
+ logUseOfReservedWordAsIdentifier( getCurrentToken() );
}
;
-
diff --git a/extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java b/extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java
index 495629e822eac2..258cb1d9718295 100644
--- a/extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java
+++ b/extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java
@@ -8,10 +8,10 @@
import io.quarkus.panacheql.internal.HqlLexer;
import io.quarkus.panacheql.internal.HqlParser;
import io.quarkus.panacheql.internal.HqlParser.AndPredicateContext;
-import io.quarkus.panacheql.internal.HqlParser.EqualityPredicateContext;
+import io.quarkus.panacheql.internal.HqlParser.ComparisonPredicateContext;
+import io.quarkus.panacheql.internal.HqlParser.GeneralPathExpressionContext;
import io.quarkus.panacheql.internal.HqlParser.IsNullPredicateContext;
import io.quarkus.panacheql.internal.HqlParser.LiteralExpressionContext;
-import io.quarkus.panacheql.internal.HqlParser.PathExpressionContext;
import io.quarkus.panacheql.internal.HqlParser.PredicateContext;
import io.quarkus.panacheql.internal.HqlParserBaseVisitor;
@@ -47,8 +47,11 @@ public String visitIsNullPredicate(IsNullPredicateContext ctx) {
}
@Override
- public String visitEqualityPredicate(EqualityPredicateContext ctx) {
- return ctx.expression(0).accept(this) + " == " + ctx.expression(1).accept(this);
+ public String visitComparisonPredicate(ComparisonPredicateContext ctx) {
+ if (ctx.comparisonOperator().EQUAL() != null) {
+ return ctx.expression(0).accept(this) + " == " + ctx.expression(1).accept(this);
+ }
+ return super.visitComparisonPredicate(ctx);
}
@Override
@@ -57,7 +60,7 @@ public String visitLiteralExpression(LiteralExpressionContext ctx) {
}
@Override
- public String visitPathExpression(PathExpressionContext ctx) {
+ public String visitGeneralPathExpression(GeneralPathExpressionContext ctx) {
return ctx.getText();
}
};
diff --git a/extensions/pom.xml b/extensions/pom.xml
index f405528ad17822..5dd8c802a2f680 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -38,6 +38,7 @@
vertx-httpundertowwebsockets
+ websockets-nextwebjars-locatorresteasy-reactivereactive-routes
@@ -223,49 +224,18 @@
maven-enforcer-plugin
- enforce-no-runtime-deps
+ enforceenforce
-
+
-
-
- io.quarkus
- quarkus-enforcer-rules
- ${project.version}
-
-
-
-
-
- com.gradle
- gradle-enterprise-maven-extension
-
-
-
-
-
- maven-compiler-plugin
-
- the extension config doc generation tool shares data across all extensions
-
-
-
-
-
-
-
-
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java
new file mode 100644
index 00000000000000..9a2943c1ab78ac
--- /dev/null
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java
@@ -0,0 +1,216 @@
+package io.quarkus.quartz.test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.Dependent;
+import jakarta.inject.Inject;
+
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.SimpleScheduleBuilder;
+import org.quartz.Trigger;
+import org.quartz.TriggerBuilder;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class DependentBeanJobTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(Service.class, MyJob.class, RefiringJob.class)
+ .addAsResource(new StringAsset("quarkus.quartz.start-mode=forced"),
+ "application.properties"));
+
+ @Inject
+ Scheduler quartz;
+
+ @Inject
+ Service service;
+
+ @Test
+ public void testDependentBeanJobDestroyed() throws SchedulerException, InterruptedException {
+ // prepare latches, schedule 10 one-off jobs, assert
+ CountDownLatch execLatch = service.initExecuteLatch(10);
+ CountDownLatch constructLatch = service.initConstructLatch(10);
+ CountDownLatch destroyedLatch = service.initDestroyedLatch(10);
+ for (int i = 0; i < 10; i++) {
+ Trigger trigger = TriggerBuilder.newTrigger()
+ .withIdentity("myTrigger" + i, "myGroup")
+ .startNow()
+ .build();
+ JobDetail job = JobBuilder.newJob(MyJob.class)
+ .withIdentity("myJob" + i, "myGroup")
+ .build();
+ quartz.scheduleJob(job, trigger);
+ }
+ assertTrue(execLatch.await(2, TimeUnit.SECONDS), "Latch count: " + execLatch.getCount());
+ assertTrue(constructLatch.await(2, TimeUnit.SECONDS), "Latch count: " + constructLatch.getCount());
+ assertTrue(destroyedLatch.await(2, TimeUnit.SECONDS), "Latch count: " + destroyedLatch.getCount());
+
+ // now try the same with repeating job triggering three times
+ execLatch = service.initExecuteLatch(3);
+ constructLatch = service.initConstructLatch(3);
+ destroyedLatch = service.initDestroyedLatch(3);
+ JobDetail job = JobBuilder.newJob(MyJob.class)
+ .withIdentity("myRepeatingJob", "myGroup")
+ .build();
+ Trigger trigger = TriggerBuilder.newTrigger()
+ .withIdentity("myRepeatingTrigger", "myGroup")
+ .startNow()
+ .withSchedule(
+ SimpleScheduleBuilder.simpleSchedule()
+ .withIntervalInMilliseconds(333)
+ .withRepeatCount(2))
+ .build();
+ quartz.scheduleJob(job, trigger);
+
+ assertTrue(execLatch.await(2, TimeUnit.SECONDS), "Latch count: " + execLatch.getCount());
+ assertTrue(constructLatch.await(2, TimeUnit.SECONDS), "Latch count: " + constructLatch.getCount());
+ assertTrue(destroyedLatch.await(2, TimeUnit.SECONDS), "Latch count: " + destroyedLatch.getCount());
+ }
+
+ @Test
+ public void testDependentBeanJobWithRefire() throws SchedulerException, InterruptedException {
+ // 5 one-off jobs should trigger construction/execution/destruction 10 times in total
+ CountDownLatch execLatch = service.initExecuteLatch(10);
+ CountDownLatch constructLatch = service.initConstructLatch(10);
+ CountDownLatch destroyedLatch = service.initDestroyedLatch(10);
+ for (int i = 0; i < 5; i++) {
+ Trigger trigger = TriggerBuilder.newTrigger()
+ .withIdentity("myTrigger" + i, "myRefiringGroup")
+ .startNow()
+ .build();
+ JobDetail job = JobBuilder.newJob(RefiringJob.class)
+ .withIdentity("myJob" + i, "myRefiringGroup")
+ .build();
+ quartz.scheduleJob(job, trigger);
+ }
+ assertTrue(execLatch.await(2, TimeUnit.SECONDS), "Latch count: " + execLatch.getCount());
+ assertTrue(constructLatch.await(2, TimeUnit.SECONDS), "Latch count: " + constructLatch.getCount());
+ assertTrue(destroyedLatch.await(2, TimeUnit.SECONDS), "Latch count: " + destroyedLatch.getCount());
+
+ // repeating job triggering three times; we expect six beans to exist for that due to refires
+ execLatch = service.initExecuteLatch(6);
+ constructLatch = service.initConstructLatch(6);
+ destroyedLatch = service.initDestroyedLatch(6);
+ JobDetail job = JobBuilder.newJob(RefiringJob.class)
+ .withIdentity("myRepeatingJob", "myRefiringGroup")
+ .build();
+ Trigger trigger = TriggerBuilder.newTrigger()
+ .withIdentity("myRepeatingTrigger", "myRefiringGroup")
+ .startNow()
+ .withSchedule(
+ SimpleScheduleBuilder.simpleSchedule()
+ .withIntervalInMilliseconds(333)
+ .withRepeatCount(2))
+ .build();
+ quartz.scheduleJob(job, trigger);
+
+ assertTrue(execLatch.await(2, TimeUnit.SECONDS), "Latch count: " + execLatch.getCount());
+ assertTrue(constructLatch.await(2, TimeUnit.SECONDS), "Latch count: " + constructLatch.getCount());
+ assertTrue(destroyedLatch.await(2, TimeUnit.SECONDS), "Latch count: " + destroyedLatch.getCount());
+ }
+
+ @ApplicationScoped
+ public static class Service {
+
+ volatile CountDownLatch executeLatch;
+ volatile CountDownLatch constructedLatch;
+ volatile CountDownLatch destroyedLatch;
+
+ public CountDownLatch initExecuteLatch(int latchCountdown) {
+ this.executeLatch = new CountDownLatch(latchCountdown);
+ return executeLatch;
+ }
+
+ public CountDownLatch initConstructLatch(int latchCountdown) {
+ this.constructedLatch = new CountDownLatch(latchCountdown);
+ return constructedLatch;
+ }
+
+ public CountDownLatch initDestroyedLatch(int latchCountdown) {
+ this.destroyedLatch = new CountDownLatch(latchCountdown);
+ return destroyedLatch;
+ }
+
+ public void execute() {
+ executeLatch.countDown();
+ }
+
+ public void constructedLatch() {
+ constructedLatch.countDown();
+ }
+
+ public void destroyedLatch() {
+ destroyedLatch.countDown();
+ }
+
+ }
+
+ @Dependent
+ static class MyJob implements Job {
+
+ @Inject
+ Service service;
+
+ @PostConstruct
+ void postConstruct() {
+ service.constructedLatch();
+ }
+
+ @PreDestroy
+ void preDestroy() {
+ service.destroyedLatch();
+ }
+
+ @Override
+ public void execute(JobExecutionContext context) throws JobExecutionException {
+ service.execute();
+ }
+ }
+
+ @Dependent
+ static class RefiringJob implements Job {
+
+ @Inject
+ Service service;
+
+ @PostConstruct
+ void postConstruct() {
+ service.constructedLatch();
+ }
+
+ @PreDestroy
+ void preDestroy() {
+ service.destroyedLatch();
+ }
+
+ @Override
+ public void execute(JobExecutionContext context) throws JobExecutionException {
+ if (context.getRefireCount() == 0) {
+ service.execute();
+ // request re-fire; we expect a new dependent bean to be used for that
+ throw new JobExecutionException("Refiring job", true);
+ } else {
+ service.execute();
+ // no re-fire the second time
+ throw new JobExecutionException("Job was re-fired successfully", false);
+ }
+ }
+ }
+}
diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java
new file mode 100644
index 00000000000000..23f40652349064
--- /dev/null
+++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java
@@ -0,0 +1,37 @@
+package io.quarkus.quartz.runtime;
+
+import jakarta.enterprise.context.Dependent;
+import jakarta.enterprise.inject.Instance;
+
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.Scheduler;
+import org.quartz.spi.TriggerFiredBundle;
+
+/**
+ * An abstraction allowing proper destruction of Job instances in case they are dependent beans.
+ * According to {@link org.quartz.spi.JobFactory#newJob(TriggerFiredBundle, Scheduler)}, a new job instance is created for every
+ * trigger.
+ * We will therefore create a new dependent bean for every trigger and destroy it afterwards.
+ */
+class CdiAwareJob implements Job {
+
+ private final Instance extends Job> jobInstance;
+
+ public CdiAwareJob(Instance extends Job> jobInstance) {
+ this.jobInstance = jobInstance;
+ }
+
+ @Override
+ public void execute(JobExecutionContext context) throws JobExecutionException {
+ Instance.Handle extends Job> handle = jobInstance.getHandle();
+ try {
+ handle.get().execute(context);
+ } finally {
+ if (handle.getBean().getScope().equals(Dependent.class)) {
+ handle.destroy();
+ }
+ }
+ }
+}
diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java
index 60e76ea042e8e7..7ea756c6a5fb46 100644
--- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java
+++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java
@@ -1243,10 +1243,10 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler Scheduler) thr
// Get the original class from an intercepted bean class
jobClass = (Class extends Job>) jobClass.getSuperclass();
}
- Instance> instance = jobs.select(jobClass);
+ Instance extends Job> instance = jobs.select(jobClass);
if (instance.isResolvable()) {
// This is a job backed by a CDI bean
- return jobWithSpanWrapper((Job) instance.get());
+ return jobWithSpanWrapper(new CdiAwareJob(instance));
}
// Instantiate a plain job class
return jobWithSpanWrapper(super.newJob(bundle, Scheduler));
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 c2fc7b73931673..e2ab7c880eaed0 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
@@ -41,6 +41,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import jakarta.inject.Singleton;
+
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
@@ -77,6 +79,7 @@
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
+import io.quarkus.deployment.IsTest;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
@@ -109,6 +112,7 @@
import io.quarkus.qute.ParameterDeclaration;
import io.quarkus.qute.ParserHelper;
import io.quarkus.qute.ParserHook;
+import io.quarkus.qute.RenderedResults;
import io.quarkus.qute.ResultNode;
import io.quarkus.qute.SectionHelper;
import io.quarkus.qute.SectionHelperFactory;
@@ -148,6 +152,7 @@
import io.quarkus.qute.runtime.extensions.OrOperatorTemplateExtensions;
import io.quarkus.qute.runtime.extensions.StringTemplateExtensions;
import io.quarkus.qute.runtime.extensions.TimeTemplateExtensions;
+import io.quarkus.qute.runtime.test.RenderedResultsCreator;
import io.quarkus.runtime.util.StringUtil;
public class QuteProcessor {
@@ -874,6 +879,18 @@ void validateCheckedFragments(List validatio
}
}
+ @BuildStep(onlyIf = IsTest.class)
+ SyntheticBeanBuildItem registerRenderedResults(QuteConfig config) {
+ if (config.testMode.recordRenderedResults) {
+ return SyntheticBeanBuildItem.configure(RenderedResults.class)
+ .unremovable()
+ .scope(Singleton.class)
+ .creator(RenderedResultsCreator.class)
+ .done();
+ }
+ return null;
+ }
+
@SuppressWarnings("incomplete-switch")
private static String getCheckedTemplateParameterTypeName(Type type) {
switch (type.kind()) {
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/FooTemplates.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/FooTemplates.java
new file mode 100644
index 00000000000000..3aa4576ca31684
--- /dev/null
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/FooTemplates.java
@@ -0,0 +1,12 @@
+package io.quarkus.qute.deployment.test;
+
+import io.quarkus.qute.CheckedTemplate;
+import io.quarkus.qute.TemplateInstance;
+
+@CheckedTemplate
+public class FooTemplates {
+
+ static native TemplateInstance foo(String name);
+
+ static native TemplateInstance foo$bar();
+}
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsDisabledTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsDisabledTest.java
new file mode 100644
index 00000000000000..867778d4ee832c
--- /dev/null
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsDisabledTest.java
@@ -0,0 +1,33 @@
+package io.quarkus.qute.deployment.test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import jakarta.enterprise.inject.Instance;
+import jakarta.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.RenderedResults;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class RenderedResultsDisabledTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot(root -> root
+ .addClasses(SimpleBean.class)
+ .addAsResource(new StringAsset("quarkus.qute.test-mode.record-rendered-results=false"),
+ "application.properties")
+ .addAsResource(new StringAsset("{name}"), "templates/foo.txt"));
+
+ @Inject
+ Instance renderedResults;
+
+ @Test
+ public void testRenderedResultsNotRegistered() {
+ assertTrue(renderedResults.isUnsatisfied());
+ }
+
+}
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsTest.java
new file mode 100644
index 00000000000000..3c15c854ebc249
--- /dev/null
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsTest.java
@@ -0,0 +1,98 @@
+package io.quarkus.qute.deployment.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import jakarta.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.RenderedResults;
+import io.quarkus.qute.RenderedResults.RenderedResult;
+import io.quarkus.qute.TemplateInstance;
+import io.quarkus.qute.Variant;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class RenderedResultsTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot(root -> root
+ .addClasses(SimpleBean.class, FooTemplates.class)
+ .addAsResource(new StringAsset("quarkus.qute.suffixes=txt,html"), "application.properties")
+ .addAsResource(new StringAsset("{name}{#fragment id=bar rendered=false}bar{/fragment}"),
+ "templates/foo.txt")
+ .addAsResource(new StringAsset("
`;
}
diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js
index 1fd8e109b5db8f..45470905e47b4f 100644
--- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js
+++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js
@@ -1,14 +1,16 @@
import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element';
import { devServices } from 'devui-data';
+import { observeState } from 'lit-element-state';
+import { themeState } from 'theme-state';
import '@vaadin/icon';
-import 'qui-code-block';
+import '@quarkus-webcomponents/codeblock';
import 'qui-card';
import 'qwc-no-data';
/**
* This component shows the Dev Services Page
*/
-export class QwcDevServices extends QwcHotReloadElement {
+export class QwcDevServices extends observeState(QwcHotReloadElement) {
static styles = css`
.cards {
height: 100%;
@@ -100,6 +102,7 @@ export class QwcDevServices extends QwcHotReloadElement {
`;
diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js
index a0baff193e6c5d..3b67934b3185a4 100644
--- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js
+++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js
@@ -1,14 +1,16 @@
import { LitElement, html, css} from 'lit';
import { RouterController } from 'router-controller';
import { JsonRpc } from 'jsonrpc';
+import { observeState } from 'lit-element-state';
+import { themeState } from 'theme-state';
import '@vaadin/icon';
-import 'qui-code-block';
+import '@quarkus-webcomponents/codeblock';
import '@vaadin/progress-bar';
/**
* This component loads an external page
*/
-export class QwcExternalPage extends LitElement {
+export class QwcExternalPage extends observeState(LitElement) {
routerController = new RouterController(this);
static styles = css`
@@ -115,7 +117,8 @@ export class QwcExternalPage extends LitElement {
+ src='${this._externalUrl}'
+ theme='${themeState.theme.name}'>
`;
diff --git a/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java b/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java
index 45791f5e27b862..2c6e51397d64d6 100644
--- a/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java
+++ b/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java
@@ -149,10 +149,10 @@ private JsonNode objectResultFromJsonRPC(int id) throws InterruptedException, Js
}
private JsonNode objectResultFromJsonRPC(int id, int loopCount) throws InterruptedException, JsonProcessingException {
- if (MESSAGES.containsKey(id)) {
- String response = MESSAGES.remove(id);
+ if (RESPONSES.containsKey(id)) {
+ WebSocketResponse response = RESPONSES.remove(id);
if (response != null) {
- ObjectNode json = (ObjectNode) new ObjectMapper().readTree(response);
+ ObjectNode json = (ObjectNode) new ObjectMapper().readTree(response.message());
JsonNode result = json.get("result");
if (result != null) {
return result.get("object");
@@ -212,25 +212,48 @@ private int sendRequest(String methodName, Map params) throws IO
socket.frameHandler((e) -> {
Buffer b = accumulatedBuffer.appendBuffer(e.binaryData());
if (e.isFinal()) {
- MESSAGES.put(id, b.toString());
+ RESPONSES.put(id, new WebSocketResponse(b.toString()));
}
});
socket.writeTextMessage(request);
socket.exceptionHandler((e) -> {
- e.printStackTrace();
+ RESPONSES.put(id, new WebSocketResponse(e));
vertx.close();
});
socket.closeHandler(v -> {
vertx.close();
});
} else {
+ RESPONSES.put(id, new WebSocketResponse(ar.cause()));
vertx.close();
}
});
return id;
}
- private static final ConcurrentHashMap MESSAGES = new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap RESPONSES = new ConcurrentHashMap<>();
+
+ private static class WebSocketResponse {
+ private final String message;
+ private final Throwable throwable;
+
+ public WebSocketResponse(String message) {
+ this.message = message;
+ this.throwable = null;
+ }
+
+ public WebSocketResponse(Throwable throwable) {
+ this.message = null;
+ this.throwable = throwable;
+ }
+
+ String message() {
+ if (throwable != null) {
+ throw new IllegalStateException("Request failed: " + throwable.getMessage(), throwable);
+ }
+ return message;
+ }
+ }
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java
index c66a937cda0eb7..64958b3bbfa7a2 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java
@@ -107,10 +107,10 @@ public static interface FileExtension {
}
public static interface MimeType {
- public static final String HTML = "text/html";
- public static final String JS = "text/javascript";
+ public static final String HTML = "text/html; charset=utf-8";
+ public static final String JS = "text/javascript; charset=utf-8";
public static final String JSON = "application/json";
- public static final String CSS = "text/css";
- public static final String PLAIN = "text/plain";
+ public static final String CSS = "text/css; charset=utf-8";
+ public static final String PLAIN = "text/plain; charset=utf-8";
}
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java
index e5647531e44ec1..01e1a5f3641745 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java
@@ -120,10 +120,10 @@ private String getContentType(String filename) {
private static final String CONTENT_TYPE_JAVASCRIPT = "application/javascript";
private static final String CONTENT_TYPE_JSON = "application/json";
- private static final String CONTENT_TYPE_HTML = "text/html";
- private static final String CONTENT_TYPE_XHTML = "application/xhtml+xml";
- private static final String CONTENT_TYPE_XML = "application/xml";
- private static final String CONTENT_TYPE_CSS = "text/css";
- private static final String CONTENT_TYPE_TEXT = "text/plain";
+ private static final String CONTENT_TYPE_HTML = "text/html; charset=utf-8";
+ private static final String CONTENT_TYPE_XHTML = "application/xhtml+xml; charset=utf-8";
+ private static final String CONTENT_TYPE_XML = "application/xml; charset=utf-8";
+ private static final String CONTENT_TYPE_CSS = "text/css; charset=utf-8";
+ private static final String CONTENT_TYPE_TEXT = "text/plain; charset=utf-8";
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java
index 310437b29257aa..97713c3f2ae89e 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java
@@ -1,5 +1,6 @@
package io.quarkus.devui.runtime.comms;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -206,7 +207,19 @@ private void route(JsonRpcRequest jsonRpcRequest, ServerWebSocket s) {
MessageType.Response);
}
}, failure -> {
- codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, failure);
+ Throwable actualFailure;
+ // If the jsonrpc method is actually
+ // synchronous, the failure is wrapped in an
+ // InvocationTargetException, so unwrap it here
+ if (failure instanceof InvocationTargetException f) {
+ actualFailure = f.getTargetException();
+ } else if (failure.getCause() != null
+ && failure.getCause() instanceof InvocationTargetException f) {
+ actualFailure = f.getTargetException();
+ } else {
+ actualFailure = failure;
+ }
+ codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, actualFailure);
});
}
} else {
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java
index 74b8668ded172e..c5048503200cd1 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java
@@ -104,35 +104,76 @@ public class CertificateConfig {
* An optional parameter to select a specific key in the keystore.
* When SNI is disabled, and the keystore contains multiple
* keys and no alias is specified; the behavior is undefined.
+ *
+ * @deprecated Use {@link #keyStoreAlias} instead.
*/
@ConfigItem
+ @Deprecated
public Optional keyStoreKeyAlias;
+ /**
+ * An optional parameter to select a specific key in the keystore.
+ * When SNI is disabled, and the keystore contains multiple
+ * keys and no alias is specified; the behavior is undefined.
+ */
+ @ConfigItem
+ public Optional keyStoreAlias;
+
/**
* An optional parameter to define the password for the key,
* in case it is different from {@link #keyStorePassword}
* If not given, it might be retrieved from {@linkplain CredentialsProvider}.
*
* @see {@link #credentialsProvider}.
+ * @deprecated Use {@link #keyStoreAliasPassword} instead.
*/
+ @Deprecated
@ConfigItem
public Optional keyStoreKeyPassword;
+ /**
+ * An optional parameter to define the password for the key,
+ * in case it is different from {@link #keyStorePassword}
+ * If not given, it might be retrieved from {@linkplain CredentialsProvider}.
+ *
+ * @see {@link #credentialsProvider}.
+ */
+ @ConfigItem
+ public Optional keyStoreAliasPassword;
+
/**
* A parameter to specify a {@linkplain CredentialsProvider} property key,
- * which can be used to get the password for the key from {@linkplain CredentialsProvider}.
+ * which can be used to get the password for the alias from {@linkplain CredentialsProvider}.
*
* @see {@link #credentialsProvider}
+ * @deprecated Use {@link #keyStoreAliasPasswordKey} instead.
*/
@ConfigItem
+ @Deprecated
public Optional keyStoreKeyPasswordKey;
+ /**
+ * A parameter to specify a {@linkplain CredentialsProvider} property key,
+ * which can be used to get the password for the alias from {@linkplain CredentialsProvider}.
+ *
+ * @see {@link #credentialsProvider}
+ */
+ @ConfigItem
+ public Optional keyStoreAliasPasswordKey;
+
/**
* An optional trust store that holds the certificate information of the trusted certificates.
*/
@ConfigItem
public Optional trustStoreFile;
+ /**
+ * An optional list of trusted certificates using the PEM format.
+ * If you pass multiple files, you must use the PEM format.
+ */
+ @ConfigItem
+ public Optional> trustStoreFiles;
+
/**
* An optional parameter to specify the type of the trust store file.
* If not given, the type is automatically detected based on the file name.
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java
index b441662a85f4a6..cb2656b15b9bda 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java
@@ -67,4 +67,29 @@ public class PolicyMappingConfig {
*/
@ConfigItem(defaultValue = "false")
public boolean shared;
+
+ /**
+ * Whether permission check should be applied on all matching paths, or paths specific for the Jakarta REST resources.
+ */
+ @ConfigItem(defaultValue = "ALL")
+ public AppliesTo appliesTo;
+
+ /**
+ * Specifies additional criteria on paths that should be checked.
+ */
+ public enum AppliesTo {
+ /**
+ * Apply on all matching paths.
+ */
+ ALL,
+ /**
+ * Declares that a permission check must only be applied on the Jakarta REST request paths.
+ * Use this option to delay the permission check if an authentication mechanism is chosen with an annotation on
+ * the matching Jakarta REST endpoint. This option must be set if the following REST endpoint annotations are used:
+ *
+ *
`io.quarkus.oidc.Tenant` annotation which selects an OIDC authentication mechanism with a tenant identifier
+ *
+ */
+ JAXRS
+ }
}
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 c399eb4b2fa6a1..14d2b3a8a802c9 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
@@ -60,7 +60,7 @@
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration;
import io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers;
import io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils;
-import io.quarkus.vertx.http.runtime.options.TlsCertificateReloadUtils;
+import io.quarkus.vertx.http.runtime.options.TlsCertificateReloader;
import io.smallrye.common.vertx.VertxContext;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
@@ -170,6 +170,8 @@ private boolean uriValid(HttpServerRequest httpServerRequest) {
private static HttpServerOptions httpMainServerOptions;
private static HttpServerOptions httpMainDomainSocketOptions;
private static HttpServerOptions httpManagementServerOptions;
+
+ private static final List refresTaskIds = new CopyOnWriteArrayList<>();
final HttpBuildTimeConfig httpBuildTimeConfig;
final ManagementInterfaceBuildTimeConfig managementBuildTimeConfig;
final RuntimeValue httpConfiguration;
@@ -308,7 +310,8 @@ public void startServer(Supplier vertx, ShutdownContext shutdown,
ManagementInterfaceConfiguration managementConfig = this.managementConfiguration == null ? null
: this.managementConfiguration.getValue();
if (startSocket && (httpConfiguration.hostEnabled || httpConfiguration.domainSocketEnabled
- || managementConfig.hostEnabled || managementConfig.domainSocketEnabled)) {
+ || (managementConfig != null && managementConfig.hostEnabled)
+ || (managementConfig != null && managementConfig.domainSocketEnabled))) {
// Start the server
if (closeTask == null) {
var insecureRequestStrategy = getInsecureRequestStrategy(httpBuildTimeConfig,
@@ -622,6 +625,7 @@ private static CompletableFuture initializeManagementInterface(Vertx
}
if (httpManagementServerOptions != null) {
+
vertx.createHttpServer(httpManagementServerOptions)
.requestHandler(managementRouter)
.listen(ar -> {
@@ -629,6 +633,15 @@ private static CompletableFuture initializeManagementInterface(Vertx
managementInterfaceFuture.completeExceptionally(
new IllegalStateException("Unable to start the management interface", ar.cause()));
} else {
+ if (httpManagementServerOptions.isSsl()
+ && (managementConfig.ssl.certificate.reloadPeriod.isPresent())) {
+ long l = TlsCertificateReloader.initCertReloadingAction(
+ vertx, ar.result(), httpManagementServerOptions, managementConfig.ssl);
+ if (l != -1) {
+ refresTaskIds.add(l);
+ }
+ }
+
actualManagementPort = ar.result().actualPort();
managementInterfaceFuture.complete(ar.result());
}
@@ -809,6 +822,9 @@ public void handle(AsyncResult event) {
// shutdown the management interface
try {
+ for (Long id : refresTaskIds) {
+ TlsCertificateReloader.unschedule(vertx, id);
+ }
if (managementServer != null && !isVertxClose) {
managementServer.close(handler);
}
@@ -1188,9 +1204,9 @@ public void handle(AsyncResult event) {
portSystemProperties.set(schema, actualPort, launchMode);
}
- if (https && quarkusConfig.ssl.certificate.reloadPeriod.isPresent()) {
- long l = TlsCertificateReloadUtils.handleCertificateReloading(
- vertx, httpsServer, httpsOptions, quarkusConfig);
+ if (https && (quarkusConfig.ssl.certificate.reloadPeriod.isPresent())) {
+ long l = TlsCertificateReloader.initCertReloadingAction(
+ vertx, httpsServer, httpsOptions, quarkusConfig.ssl);
if (l != -1) {
reloadingTasks.add(l);
}
@@ -1210,7 +1226,7 @@ public void handle(AsyncResult event) {
public void stop(Promise stopFuture) {
for (Long id : reloadingTasks) {
- vertx.cancelTimer(id);
+ TlsCertificateReloader.unschedule(vertx, id);
}
final AtomicInteger remainingCount = new AtomicInteger(0);
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
index d53c444263d201..a16c972884260e 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
@@ -1,15 +1,13 @@
package io.quarkus.vertx.http.runtime.options;
+import static io.quarkus.vertx.http.runtime.options.TlsUtils.computeKeyStoreOptions;
+import static io.quarkus.vertx.http.runtime.options.TlsUtils.computeTrustOptions;
+
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import java.util.concurrent.TimeUnit;
import org.jboss.logging.Logger;
@@ -24,15 +22,11 @@
import io.quarkus.vertx.http.runtime.ServerSslConfig;
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig;
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration;
-import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.ClientAuth;
import io.vertx.core.http.Http2Settings;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpVersion;
-import io.vertx.core.net.JdkSSLEngineOptions;
-import io.vertx.core.net.KeyStoreOptions;
-import io.vertx.core.net.PemKeyCertOptions;
-import io.vertx.core.net.TrafficShapingOptions;
+import io.vertx.core.net.*;
@SuppressWarnings("OptionalIsPresent")
public class HttpServerOptionsUtils {
@@ -49,15 +43,6 @@ public static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeCo
ServerSslConfig sslConfig = httpConfiguration.ssl;
- final List keys = new ArrayList<>();
- final List certificates = new ArrayList<>();
- if (sslConfig.certificate.keyFiles.isPresent()) {
- keys.addAll(sslConfig.certificate.keyFiles.get());
- }
- if (sslConfig.certificate.files.isPresent()) {
- certificates.addAll(sslConfig.certificate.files.get());
- }
-
// credentials provider
Map credentials = Map.of();
if (sslConfig.certificate.credentialsProvider.isPresent()) {
@@ -66,14 +51,33 @@ public static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeCo
String name = sslConfig.certificate.credentialsProvider.get();
credentials = credentialsProvider.getCredentials(name);
}
- final Optional keyStoreFile = sslConfig.certificate.keyStoreFile;
+
final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials,
sslConfig.certificate.keyStorePasswordKey);
- final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials,
- sslConfig.certificate.keyStoreKeyPasswordKey);
- final Optional trustStoreFile = sslConfig.certificate.trustStoreFile;
+
+ Optional keyStoreAliasPassword = Optional.empty();
+ if (sslConfig.certificate.keyStoreAliasPassword.isPresent() || sslConfig.certificate.keyStoreKeyPassword.isPresent()
+ || sslConfig.certificate.keyStoreKeyPasswordKey.isPresent()
+ || sslConfig.certificate.keyStoreAliasPasswordKey.isPresent()) {
+ if (sslConfig.certificate.keyStoreKeyPasswordKey.isPresent()
+ && sslConfig.certificate.keyStoreAliasPasswordKey.isPresent()) {
+ throw new ConfigurationException(
+ "You cannot specify both `keyStoreKeyPasswordKey` and `keyStoreAliasPasswordKey` - Use `keyStoreAliasPasswordKey` instead");
+ }
+ if (sslConfig.certificate.keyStoreAliasPassword.isPresent()
+ && sslConfig.certificate.keyStoreKeyPassword.isPresent()) {
+ throw new ConfigurationException(
+ "You cannot specify both `keyStoreKeyPassword` and `keyStoreAliasPassword` - Use `keyStoreAliasPassword` instead");
+ }
+ keyStoreAliasPassword = getCredential(
+ or(sslConfig.certificate.keyStoreAliasPassword, sslConfig.certificate.keyStoreKeyPassword),
+ credentials,
+ or(sslConfig.certificate.keyStoreAliasPasswordKey, sslConfig.certificate.keyStoreKeyPasswordKey));
+ }
+
final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials,
sslConfig.certificate.trustStorePasswordKey);
+
final HttpServerOptions serverOptions = new HttpServerOptions();
//ssl
@@ -85,32 +89,14 @@ public static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeCo
}
setIdleTimeout(httpConfiguration, serverOptions);
- if (!certificates.isEmpty() && !keys.isEmpty()) {
- createPemKeyCertOptions(certificates, keys, serverOptions);
- } else if (keyStoreFile.isPresent()) {
-
- KeyStoreOptions options = createKeyStoreOptions(
- keyStoreFile.get(),
- keyStorePassword.orElse("password"),
- sslConfig.certificate.keyStoreFileType,
- sslConfig.certificate.keyStoreProvider,
- sslConfig.certificate.keyStoreKeyAlias,
- keyStoreKeyPassword);
- serverOptions.setKeyCertOptions(options);
+ var kso = computeKeyStoreOptions(sslConfig.certificate, keyStorePassword, keyStoreAliasPassword);
+ if (kso != null) {
+ serverOptions.setKeyCertOptions(kso);
}
- if (trustStoreFile.isPresent()) {
- if (!trustStorePassword.isPresent()) {
- throw new IllegalArgumentException("No trust store password provided");
- }
- KeyStoreOptions options = createKeyStoreOptions(
- trustStoreFile.get(),
- trustStorePassword.get(),
- sslConfig.certificate.trustStoreFileType,
- sslConfig.certificate.trustStoreProvider,
- sslConfig.certificate.trustStoreCertAlias,
- Optional.empty());
- serverOptions.setTrustOptions(options);
+ var to = computeTrustOptions(sslConfig.certificate, trustStorePassword);
+ if (to != null) {
+ serverOptions.setTrustOptions(to);
}
for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) {
@@ -143,15 +129,6 @@ public static HttpServerOptions createSslOptionsForManagementInterface(Managemen
ServerSslConfig sslConfig = httpConfiguration.ssl;
- final List keys = new ArrayList<>();
- final List certificates = new ArrayList<>();
- if (sslConfig.certificate.keyFiles.isPresent()) {
- keys.addAll(sslConfig.certificate.keyFiles.get());
- }
- if (sslConfig.certificate.files.isPresent()) {
- certificates.addAll(sslConfig.certificate.files.get());
- }
-
// credentials provider
Map credentials = Map.of();
if (sslConfig.certificate.credentialsProvider.isPresent()) {
@@ -160,14 +137,33 @@ public static HttpServerOptions createSslOptionsForManagementInterface(Managemen
String name = sslConfig.certificate.credentialsProvider.get();
credentials = credentialsProvider.getCredentials(name);
}
- final Optional keyStoreFile = sslConfig.certificate.keyStoreFile;
+
final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials,
sslConfig.certificate.keyStorePasswordKey);
- final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials,
- sslConfig.certificate.keyStoreKeyPasswordKey);
- final Optional trustStoreFile = sslConfig.certificate.trustStoreFile;
+
+ Optional keyStoreAliasPassword = Optional.empty();
+ if (sslConfig.certificate.keyStoreAliasPassword.isPresent() || sslConfig.certificate.keyStoreKeyPassword.isPresent()
+ || sslConfig.certificate.keyStoreKeyPasswordKey.isPresent()
+ || sslConfig.certificate.keyStoreAliasPasswordKey.isPresent()) {
+ if (sslConfig.certificate.keyStoreKeyPasswordKey.isPresent()
+ && sslConfig.certificate.keyStoreAliasPasswordKey.isPresent()) {
+ throw new ConfigurationException(
+ "You cannot specify both `keyStoreKeyPasswordKey` and `keyStoreAliasPasswordKey` - Use `keyStoreAliasPasswordKey` instead");
+ }
+ if (sslConfig.certificate.keyStoreAliasPassword.isPresent()
+ && sslConfig.certificate.keyStoreKeyPassword.isPresent()) {
+ throw new ConfigurationException(
+ "You cannot specify both `keyStoreKeyPassword` and `keyStoreAliasPassword` - Use `keyStoreAliasPassword` instead");
+ }
+ keyStoreAliasPassword = getCredential(
+ or(sslConfig.certificate.keyStoreAliasPassword, sslConfig.certificate.keyStoreKeyPassword),
+ credentials,
+ or(sslConfig.certificate.keyStoreAliasPasswordKey, sslConfig.certificate.keyStoreKeyPasswordKey));
+ }
+
final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials,
sslConfig.certificate.trustStorePasswordKey);
+
final HttpServerOptions serverOptions = new HttpServerOptions();
//ssl
@@ -179,31 +175,14 @@ public static HttpServerOptions createSslOptionsForManagementInterface(Managemen
serverOptions.setIdleTimeout(idleTimeout);
serverOptions.setIdleTimeoutUnit(TimeUnit.MILLISECONDS);
- if (!certificates.isEmpty() && !keys.isEmpty()) {
- createPemKeyCertOptions(certificates, keys, serverOptions);
- } else if (keyStoreFile.isPresent()) {
- KeyStoreOptions options = createKeyStoreOptions(
- keyStoreFile.get(),
- keyStorePassword.orElse("password"),
- sslConfig.certificate.keyStoreFileType,
- sslConfig.certificate.keyStoreProvider,
- sslConfig.certificate.keyStoreKeyAlias,
- keyStoreKeyPassword);
- serverOptions.setKeyCertOptions(options);
+ var kso = computeKeyStoreOptions(sslConfig.certificate, keyStorePassword, keyStoreAliasPassword);
+ if (kso != null) {
+ serverOptions.setKeyCertOptions(kso);
}
- if (trustStoreFile.isPresent()) {
- if (!trustStorePassword.isPresent()) {
- throw new IllegalArgumentException("No trust store password provided");
- }
- KeyStoreOptions options = createKeyStoreOptions(
- trustStoreFile.get(),
- trustStorePassword.get(),
- sslConfig.certificate.trustStoreFileType,
- sslConfig.certificate.trustStoreProvider,
- sslConfig.certificate.trustStoreCertAlias,
- Optional.empty());
- serverOptions.setTrustOptions(options);
+ var to = computeTrustOptions(sslConfig.certificate, trustStorePassword);
+ if (to != null) {
+ serverOptions.setTrustOptions(to);
}
for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) {
@@ -350,26 +329,6 @@ public static void applyCommonOptionsForManagementInterface(HttpServerOptions op
options.setUseProxyProtocol(httpConfiguration.proxy.useProxyProtocol);
}
- private static KeyStoreOptions createKeyStoreOptions(Path path, String password, Optional fileType,
- Optional provider, Optional alias, Optional aliasPassword) throws IOException {
- final String type;
- if (fileType.isPresent()) {
- type = fileType.get().toLowerCase();
- } else {
- type = findKeystoreFileType(path);
- }
-
- byte[] data = getFileContent(path);
- KeyStoreOptions options = new KeyStoreOptions()
- .setPassword(password)
- .setValue(Buffer.buffer(data))
- .setType(type.toUpperCase())
- .setProvider(provider.orElse(null))
- .setAlias(alias.orElse(null))
- .setAliasPassword(aliasPassword.orElse(null));
- return options;
- }
-
static byte[] getFileContent(Path path) throws IOException {
byte[] data;
final InputStream resource = Thread.currentThread().getContextClassLoader()
@@ -386,43 +345,6 @@ static byte[] getFileContent(Path path) throws IOException {
return data;
}
- private static void createPemKeyCertOptions(List certFile, List keyFile,
- HttpServerOptions serverOptions) throws IOException {
-
- if (certFile.size() != keyFile.size()) {
- throw new ConfigurationException("Invalid certificate configuration - `files` and `keyFiles` must have the "
- + "same number of elements");
- }
-
- List certificates = new ArrayList<>();
- List keys = new ArrayList<>();
-
- for (Path p : certFile) {
- final byte[] cert = getFileContent(p);
- certificates.add(Buffer.buffer(cert));
- }
-
- for (Path p : keyFile) {
- final byte[] key = getFileContent(p);
- keys.add(Buffer.buffer(key));
- }
-
- PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
- .setCertValues(certificates)
- .setKeyValues(keys);
- serverOptions.setPemKeyCertOptions(pemKeyCertOptions);
- }
-
- private static String findKeystoreFileType(Path storePath) {
- final String pathName = storePath.toString();
- if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) {
- return "pkcs12";
- } else {
- // assume jks
- return "jks";
- }
- }
-
private static byte[] doRead(InputStream is) throws IOException {
return is.readAllBytes();
}
@@ -453,4 +375,9 @@ public static HttpConfiguration.InsecureRequests getInsecureRequestStrategy(Http
}
return HttpConfiguration.InsecureRequests.ENABLED;
}
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ static Optional or(Optional a, Optional b) {
+ return a.isPresent() ? a : b;
+ }
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java
deleted file mode 100644
index 2acf224b5d5c8f..00000000000000
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package io.quarkus.vertx.http.runtime.options;
-
-import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.getFileContent;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.function.Function;
-
-import org.jboss.logging.Logger;
-
-import io.quarkus.vertx.http.runtime.HttpConfiguration;
-import io.vertx.core.AsyncResult;
-import io.vertx.core.Future;
-import io.vertx.core.Handler;
-import io.vertx.core.Vertx;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.http.HttpServer;
-import io.vertx.core.http.HttpServerOptions;
-import io.vertx.core.net.KeyStoreOptions;
-import io.vertx.core.net.PemKeyCertOptions;
-import io.vertx.core.net.SSLOptions;
-
-/**
- * Utility class to handle TLS certificate reloading.
- */
-public class TlsCertificateReloadUtils {
-
- public static long handleCertificateReloading(Vertx vertx, HttpServer server,
- HttpServerOptions options, HttpConfiguration configuration) {
- // Validation
- if (configuration.ssl.certificate.reloadPeriod.isEmpty()) {
- return -1;
- }
- if (configuration.ssl.certificate.reloadPeriod.get().toMillis() < 30_000) {
- throw new IllegalArgumentException(
- "Unable to configure TLS reloading - The reload period cannot be less than 30 seconds");
- }
- if (options == null) {
- throw new IllegalArgumentException("Unable to configure TLS reloading - The HTTP server options were not provided");
- }
- SSLOptions ssl = options.getSslOptions();
- if (ssl == null) {
- throw new IllegalArgumentException("Unable to configure TLS reloading - TLS/SSL is not enabled on the server");
- }
-
- Logger log = Logger.getLogger(TlsCertificateReloadUtils.class);
- return vertx.setPeriodic(configuration.ssl.certificate.reloadPeriod.get().toMillis(), new Handler() {
- @Override
- public void handle(Long id) {
-
- vertx.executeBlocking(new Callable() {
- @Override
- public SSLOptions call() throws Exception {
- // We are reading files - must be done on a worker thread.
- var c = reloadFileContent(ssl, configuration);
- if (c.equals(ssl)) { // No change, skip the update
- return null;
- }
- return c;
- }
- }, true)
- .flatMap(new Function>() {
- @Override
- public Future apply(SSLOptions res) {
- if (res != null) {
- return server.updateSSLOptions(res);
- } else {
- return Future.succeededFuture(false);
- }
- }
- })
- .onComplete(new Handler>() {
- @Override
- public void handle(AsyncResult ar) {
- if (ar.failed()) {
- log.error("Unable to reload the TLS certificate, keeping the current one.", ar.cause());
- } else {
- if (ar.result()) {
- log.debug("TLS certificates updated");
- }
- // Not updated, no change.
- }
- }
- });
- }
- });
- }
-
- private static SSLOptions reloadFileContent(SSLOptions ssl, HttpConfiguration configuration) throws IOException {
- var copy = new SSLOptions(ssl);
-
- final List keys = new ArrayList<>();
- final List certificates = new ArrayList<>();
-
- if (configuration.ssl.certificate.keyFiles.isPresent()) {
- keys.addAll(configuration.ssl.certificate.keyFiles.get());
- }
- if (configuration.ssl.certificate.files.isPresent()) {
- certificates.addAll(configuration.ssl.certificate.files.get());
- }
-
- if (!certificates.isEmpty() && !keys.isEmpty()) {
- List certBuffer = new ArrayList<>();
- List keysBuffer = new ArrayList<>();
-
- for (Path p : certificates) {
- byte[] cert = getFileContent(p);
- certBuffer.add(Buffer.buffer(cert));
- }
- for (Path p : keys) {
- byte[] key = getFileContent(p);
- keysBuffer.add(Buffer.buffer(key));
- }
-
- PemKeyCertOptions opts = new PemKeyCertOptions()
- .setCertValues(certBuffer)
- .setKeyValues(keysBuffer);
- copy.setKeyCertOptions(opts);
- } else if (configuration.ssl.certificate.keyStoreFile.isPresent()) {
- var opts = ((KeyStoreOptions) copy.getKeyCertOptions());
- opts.setValue(Buffer.buffer(getFileContent(configuration.ssl.certificate.keyStoreFile.get())));
- copy.setKeyCertOptions(opts);
- }
-
- if (configuration.ssl.certificate.trustStoreFile.isPresent()) {
- var opts = ((KeyStoreOptions) copy.getKeyCertOptions());
- opts.setValue(Buffer.buffer(getFileContent(configuration.ssl.certificate.trustStoreFile.get())));
- copy.setTrustOptions(opts);
- }
-
- return copy;
- }
-}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java
new file mode 100644
index 00000000000000..fcf993863b7392
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java
@@ -0,0 +1,228 @@
+package io.quarkus.vertx.http.runtime.options;
+
+import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.getFileContent;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.vertx.http.runtime.ServerSslConfig;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.net.KeyStoreOptions;
+import io.vertx.core.net.PemKeyCertOptions;
+import io.vertx.core.net.SSLOptions;
+
+/**
+ * Utility class to handle TLS certificate reloading.
+ */
+public class TlsCertificateReloader {
+
+ /**
+ * A structure storing the reload tasks.
+ */
+ private static final List TASKS = new CopyOnWriteArrayList<>();
+
+ private static final Logger LOGGER = Logger.getLogger(TlsCertificateReloader.class);
+
+ public static long initCertReloadingAction(Vertx vertx, HttpServer server,
+ HttpServerOptions options, ServerSslConfig configuration) {
+
+ if (options == null) {
+ throw new IllegalArgumentException("Unable to configure TLS reloading - The HTTP server options were not provided");
+ }
+ SSLOptions ssl = options.getSslOptions();
+ if (ssl == null) {
+ throw new IllegalArgumentException("Unable to configure TLS reloading - TLS/SSL is not enabled on the server");
+ }
+
+ long period;
+ // Validation
+ if (configuration.certificate.reloadPeriod.isPresent()) {
+ if (configuration.certificate.reloadPeriod.get().toMillis() < 30_000) {
+ throw new IllegalArgumentException(
+ "Unable to configure TLS reloading - The reload period cannot be less than 30 seconds");
+ }
+ period = configuration.certificate.reloadPeriod.get().toMillis();
+ } else {
+ return -1;
+ }
+
+ Supplier> task = new Supplier>() {
+ @Override
+ public CompletionStage get() {
+
+ Future future = vertx.executeBlocking(new Callable() {
+ @Override
+ public SSLOptions call() throws Exception {
+ // We are reading files - must be done on a worker thread.
+ var c = reloadFileContent(ssl, configuration);
+ if (c.equals(ssl)) { // No change, skip the update
+ return null;
+ }
+ return c;
+ }
+ }, true)
+ .flatMap(new Function>() {
+ @Override
+ public Future apply(SSLOptions res) {
+ if (res != null) {
+ return server.updateSSLOptions(res);
+ } else {
+ return Future.succeededFuture(false);
+ }
+ }
+ })
+ .onComplete(new Handler>() {
+ @Override
+ public void handle(AsyncResult ar) {
+ if (ar.failed()) {
+ LOGGER.error("Unable to reload the TLS certificate, keeping the current one.", ar.cause());
+ } else {
+ if (ar.result()) {
+ LOGGER.debug("TLS certificates updated");
+ }
+ // Not updated, no change.
+ }
+ }
+ });
+
+ return future.toCompletionStage();
+ }
+ };
+
+ long id = vertx.setPeriodic(period, new Handler() {
+ @Override
+ public void handle(Long id) {
+ task.get();
+ }
+ });
+
+ TASKS.add(new ReloadCertificateTask(id, task));
+ return id;
+ }
+
+ public static void unschedule(Vertx vertx, long id) {
+ vertx.cancelTimer(id);
+ for (ReloadCertificateTask task : TASKS) {
+ if (task.it == id) {
+ TASKS.remove(task);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Trigger all the reload tasks.
+ * This method is NOT part of the public API, and is only used for testing purpose.
+ *
+ * @return a Uni that is completed when all the reload tasks have been executed
+ */
+ public static CompletionStage reload() {
+ @SuppressWarnings("rawtypes")
+ CompletableFuture[] futures = new CompletableFuture[TASKS.size()];
+ for (int i = 0; i < TASKS.size(); i++) {
+ futures[i] = TASKS.get(i).action().get().toCompletableFuture();
+ }
+
+ return CompletableFuture.allOf(futures);
+ }
+
+ private static SSLOptions reloadFileContent(SSLOptions ssl, ServerSslConfig configuration) throws IOException {
+ var copy = new SSLOptions(ssl);
+
+ final List keys = new ArrayList<>();
+ final List certificates = new ArrayList<>();
+
+ configuration.certificate.keyFiles.ifPresent(keys::addAll);
+ configuration.certificate.files.ifPresent(certificates::addAll);
+
+ if (!certificates.isEmpty() && !keys.isEmpty()) {
+ List certBuffer = new ArrayList<>();
+ List keysBuffer = new ArrayList<>();
+
+ for (Path p : certificates) {
+ byte[] cert = getFileContent(p);
+ certBuffer.add(Buffer.buffer(cert));
+ }
+ for (Path p : keys) {
+ byte[] key = getFileContent(p);
+ keysBuffer.add(Buffer.buffer(key));
+ }
+
+ PemKeyCertOptions opts = new PemKeyCertOptions()
+ .setCertValues(certBuffer)
+ .setKeyValues(keysBuffer);
+ copy.setKeyCertOptions(opts);
+ } else if (configuration.certificate.keyStoreFile.isPresent()) {
+ var opts = ((KeyStoreOptions) copy.getKeyCertOptions());
+ opts.setValue(Buffer.buffer(getFileContent(configuration.certificate.keyStoreFile.get())));
+ copy.setKeyCertOptions(opts);
+ }
+
+ if (configuration.certificate.trustStoreFile.isPresent()) {
+ var opts = ((KeyStoreOptions) copy.getKeyCertOptions());
+ opts.setValue(Buffer.buffer(getFileContent(configuration.certificate.trustStoreFile.get())));
+ copy.setTrustOptions(opts);
+ }
+
+ return copy;
+ }
+
+ static final class ReloadCertificateTask {
+ private final long it;
+ private final Supplier> action;
+
+ ReloadCertificateTask(long it, Supplier> action) {
+ this.it = it;
+ this.action = action;
+ }
+
+ public long it() {
+ return it;
+ }
+
+ public Supplier> action() {
+ return action;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this)
+ return true;
+ if (obj == null || obj.getClass() != this.getClass())
+ return false;
+ var that = (ReloadCertificateTask) obj;
+ return this.it == that.it &&
+ Objects.equals(this.action, that.action);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(it, action);
+ }
+
+ @Override
+ public String toString() {
+ return "ReloadCertificateTask[" +
+ "it=" + it + ", " +
+ "action=" + action + ']';
+ }
+
+ }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsUtils.java
new file mode 100644
index 00000000000000..cbeb165df37538
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsUtils.java
@@ -0,0 +1,181 @@
+package io.quarkus.vertx.http.runtime.options;
+
+import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.getFileContent;
+import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.or;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import io.quarkus.vertx.http.runtime.CertificateConfig;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.net.*;
+
+/**
+ * Utility class for TLS configuration.
+ */
+public class TlsUtils {
+
+ private TlsUtils() {
+ // Avoid direct instantiation
+ }
+
+ public static KeyCertOptions computeKeyStoreOptions(CertificateConfig certificates, Optional keyStorePassword,
+ Optional keyStoreAliasPassword) throws IOException {
+
+ if (certificates.keyFiles.isPresent() || certificates.files.isPresent()) {
+ if (certificates.keyFiles.isEmpty()) {
+ throw new IllegalArgumentException("You must specify the key files when specifying the certificate files");
+ }
+ if (certificates.files.isEmpty()) {
+ throw new IllegalArgumentException("You must specify the certificate files when specifying the key files");
+ }
+ if (certificates.files.get().size() != certificates.keyFiles.get().size()) {
+ throw new IllegalArgumentException(
+ "The number of certificate files and key files must be the same, and be given in the same order");
+ }
+ return createPemKeyCertOptions(certificates.files.get(), certificates.keyFiles.get());
+ } else if (certificates.keyStoreFile.isPresent()) {
+ var type = getKeyStoreType(certificates.keyStoreFile.get(), certificates.keyStoreFileType);
+ return createKeyStoreOptions(
+ certificates.keyStoreFile.get(),
+ keyStorePassword,
+ type,
+ certificates.keyStoreProvider,
+ or(certificates.keyStoreAlias, certificates.keyStoreKeyAlias),
+ keyStoreAliasPassword);
+ }
+ return null;
+ }
+
+ public static TrustOptions computeTrustOptions(CertificateConfig certificates, Optional trustStorePassword)
+ throws IOException {
+ // Decide if we have a single trust store file or multiple trust store files (PEM)
+ Path singleTrustStoreFile = getSingleTrustStoreFile(certificates);
+
+ if (singleTrustStoreFile != null) { // We have a single trust store file.
+ String type = getTruststoreType(singleTrustStoreFile, certificates.trustStoreFileType);
+ if (type.equalsIgnoreCase("pem")) {
+ byte[] cert = getFileContent(singleTrustStoreFile);
+ return new PemTrustOptions()
+ .addCertValue(Buffer.buffer(cert));
+ }
+
+ if ((type.equalsIgnoreCase("pkcs12") || type.equalsIgnoreCase("jks"))) {
+ // We cannot assume that custom type configured by the user requires a password.
+ if (certificates.trustStorePassword.isEmpty() && trustStorePassword.isEmpty()) {
+ throw new IllegalArgumentException("No trust store password provided");
+ }
+ }
+
+ return createKeyStoreOptions(
+ singleTrustStoreFile,
+ trustStorePassword,
+ type,
+ certificates.trustStoreProvider,
+ certificates.trustStoreCertAlias,
+ Optional.empty());
+ }
+
+ // We have multiple trust store files (PEM).
+ if (certificates.trustStoreFiles.isPresent() && !certificates.trustStoreFiles.get().isEmpty()) {
+ // Assuming PEM, as it's the only format with multiple files
+ PemTrustOptions pemKeyCertOptions = new PemTrustOptions();
+ for (Path path : certificates.trustStoreFiles.get()) {
+ byte[] cert = getFileContent(path);
+ pemKeyCertOptions.addCertValue(Buffer.buffer(cert));
+ }
+ return pemKeyCertOptions;
+ }
+
+ return null;
+ }
+
+ private static Path getSingleTrustStoreFile(CertificateConfig certificates) {
+ Path singleTrustStoreFile = null;
+ if (certificates.trustStoreFile.isPresent()) {
+ singleTrustStoreFile = certificates.trustStoreFile.get();
+ }
+ if (certificates.trustStoreFiles.isPresent()) {
+ if (singleTrustStoreFile != null) {
+ throw new IllegalArgumentException("You cannot specify both `trustStoreFile` and `trustStoreFiles`");
+ }
+ if (certificates.trustStoreFiles.get().size() == 1) {
+ singleTrustStoreFile = certificates.trustStoreFiles.get().get(0);
+ }
+ }
+ return singleTrustStoreFile;
+ }
+
+ static String getTruststoreType(Path singleTrustStoreFile, Optional userType) {
+ String type;
+ if (userType.isPresent()) {
+ type = userType.get().toLowerCase();
+ } else {
+ type = getTypeFromFileName("truststore", singleTrustStoreFile);
+ }
+ return type;
+ }
+
+ private static String getTypeFromFileName(String keystoreOrTruststore, Path path) {
+ String name = path.getFileName().toString().toLowerCase();
+ if (name.endsWith(".p12") || name.endsWith(".pkcs12") || name.endsWith(".pfx")) {
+ return "pkcs12";
+ } else if (name.endsWith(".jks")) {
+ return "jks";
+ } else if (name.endsWith(".key") || name.endsWith(".crt") || name.endsWith(".pem")) {
+ return "pem";
+ } else {
+ throw new IllegalArgumentException("Could not determine the " + keystoreOrTruststore
+ + " type from the file name: " + path
+ + ". Configure the `quarkus.http.ssl.certificate.[key-store|trust-store]-file-type` property.");
+
+ }
+
+ }
+
+ private static KeyStoreOptions createKeyStoreOptions(Path path, Optional password, String type,
+ Optional provider, Optional alias,
+ Optional aliasPassword) throws IOException {
+ byte[] data = getFileContent(path);
+ return new KeyStoreOptions()
+ .setPassword(password.orElse(null))
+ .setValue(Buffer.buffer(data))
+ .setType(type.toUpperCase())
+ .setProvider(provider.orElse(null))
+ .setAlias(alias.orElse(null))
+ .setAliasPassword(aliasPassword.orElse(null));
+ }
+
+ static String getKeyStoreType(Path path, Optional fileType) {
+ final String type;
+ if (fileType.isPresent()) {
+ type = fileType.get().toLowerCase();
+ } else {
+ type = getTypeFromFileName("keystore", path);
+ }
+ return type;
+ }
+
+ private static PemKeyCertOptions createPemKeyCertOptions(List certFile, List keyFile) throws IOException {
+ List certificates = new ArrayList<>();
+ List keys = new ArrayList<>();
+
+ for (Path p : certFile) {
+ final byte[] cert = getFileContent(p);
+ certificates.add(Buffer.buffer(cert));
+ }
+
+ for (Path p : keyFile) {
+ final byte[] key = getFileContent(p);
+ keys.add(Buffer.buffer(key));
+ }
+
+ return new PemKeyCertOptions()
+ .setCertValues(certificates)
+ .setKeyValues(keys);
+ }
+
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java
index b9db674190c586..1b18431ea3b269 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java
@@ -6,10 +6,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
-import java.util.function.BiFunction;
import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Supplier;
import jakarta.enterprise.event.Event;
import jakarta.enterprise.inject.spi.BeanManager;
@@ -42,8 +39,8 @@ abstract class AbstractHttpAuthorizer {
private final IdentityProviderManager identityProviderManager;
private final AuthorizationController controller;
private final List policies;
- private final BlockingSecurityExecutor blockingExecutor;
private final SecurityEventHelper securityEventHelper;
+ private final HttpSecurityPolicy.AuthorizationRequestContext context;
AbstractHttpAuthorizer(HttpAuthenticator httpAuthenticator, IdentityProviderManager identityProviderManager,
AuthorizationController controller, List policies, BeanManager beanManager,
@@ -53,33 +50,11 @@ abstract class AbstractHttpAuthorizer {
this.identityProviderManager = identityProviderManager;
this.controller = controller;
this.policies = policies;
- this.blockingExecutor = blockingExecutor;
+ this.context = new HttpSecurityPolicy.DefaultAuthorizationRequestContext(blockingExecutor);
this.securityEventHelper = new SecurityEventHelper<>(authZSuccessEvent, authZFailureEvent, AUTHORIZATION_SUCCESS,
AUTHORIZATION_FAILURE, beanManager, securityEventsEnabled);
}
- /**
- * context that allows for running blocking tasks
- */
- private final HttpSecurityPolicy.AuthorizationRequestContext CONTEXT = new HttpSecurityPolicy.AuthorizationRequestContext() {
- @Override
- public Uni runBlocking(RoutingContext context, Uni identityUni,
- BiFunction function) {
- return identityUni
- .flatMap(new Function>() {
- @Override
- public Uni extends HttpSecurityPolicy.CheckResult> apply(SecurityIdentity identity) {
- return blockingExecutor.executeBlocking(new Supplier() {
- @Override
- public HttpSecurityPolicy.CheckResult get() {
- return function.apply(context, identity);
- }
- });
- }
- });
- }
- };
-
/**
* Checks that the request is allowed to proceed. If it is then {@link RoutingContext#next()} will
* be invoked, if not appropriate action will be taken to either report the failure or attempt authentication.
@@ -121,7 +96,7 @@ && permissionCheckPerformed(permissionCheckers, routingContext, index)) {
}
//get the current checker
HttpSecurityPolicy res = permissionCheckers.get(index);
- res.checkPermission(routingContext, identity, CONTEXT)
+ res.checkPermission(routingContext, identity, context)
.subscribe().with(new Consumer() {
@Override
public void accept(HttpSecurityPolicy.CheckResult checkResult) {
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java
index ab031900716bb0..1520d7734003e7 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java
@@ -40,21 +40,29 @@ public class AbstractPathMatchingHttpSecurityPolicy {
private final List>> sharedPermissionsPathMatchers;
private final boolean hasNoPermissions;
- AbstractPathMatchingHttpSecurityPolicy(Map permissions,
- Map rolePolicy, String rootPath, Instance installedPolicies) {
- this.hasNoPermissions = permissions.isEmpty();
+ public AbstractPathMatchingHttpSecurityPolicy(Map permissions,
+ Map rolePolicy, String rootPath, Instance installedPolicies,
+ PolicyMappingConfig.AppliesTo appliesTo) {
+ boolean hasNoPermissions = permissions.isEmpty();
var namedHttpSecurityPolicies = toNamedHttpSecPolicies(rolePolicy, installedPolicies);
List>> sharedPermsMatchers = new ArrayList<>();
final var builder = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll);
- for (Map.Entry entry : permissions.entrySet()) {
- if (entry.getValue().shared) {
+ for (PolicyMappingConfig policyMappingConfig : permissions.values()) {
+ if (appliesTo != policyMappingConfig.appliesTo) {
+ continue;
+ }
+ if (hasNoPermissions) {
+ hasNoPermissions = false;
+ }
+ if (policyMappingConfig.shared) {
final var builder1 = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll);
- addPermissionToPathMatcher(namedHttpSecurityPolicies, rootPath, entry, builder1);
+ addPermissionToPathMatcher(namedHttpSecurityPolicies, rootPath, policyMappingConfig, builder1);
sharedPermsMatchers.add(builder1.build());
} else {
- addPermissionToPathMatcher(namedHttpSecurityPolicies, rootPath, entry, builder);
+ addPermissionToPathMatcher(namedHttpSecurityPolicies, rootPath, policyMappingConfig, builder);
}
}
+ this.hasNoPermissions = hasNoPermissions;
this.sharedPermissionsPathMatchers = sharedPermsMatchers.isEmpty() ? null : List.copyOf(sharedPermsMatchers);
this.pathMatcher = builder.build();
}
@@ -71,7 +79,7 @@ public String getAuthMechanismName(RoutingContext routingContext) {
return getAuthMechanismName(routingContext, pathMatcher);
}
- boolean hasNoPermissions() {
+ public boolean hasNoPermissions() {
return hasNoPermissions;
}
@@ -142,21 +150,21 @@ private static String getAuthMechanismName(RoutingContext routingContext,
}
private static void addPermissionToPathMatcher(Map permissionCheckers, String rootPath,
- Map.Entry entry,
+ PolicyMappingConfig policyMappingConfig,
ImmutablePathMatcher.ImmutablePathMatcherBuilder> builder) {
- HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy);
+ HttpSecurityPolicy checker = permissionCheckers.get(policyMappingConfig.policy);
if (checker == null) {
- throw new RuntimeException("Unable to find HTTP security policy " + entry.getValue().policy);
+ throw new RuntimeException("Unable to find HTTP security policy " + policyMappingConfig.policy);
}
- if (entry.getValue().enabled.orElse(Boolean.TRUE)) {
- for (String path : entry.getValue().paths.orElse(Collections.emptyList())) {
+ if (policyMappingConfig.enabled.orElse(Boolean.TRUE)) {
+ for (String path : policyMappingConfig.paths.orElse(Collections.emptyList())) {
path = path.trim();
if (!path.startsWith("/")) {
path = rootPath + path;
}
- HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null),
- new HashSet<>(entry.getValue().methods.orElse(Collections.emptyList())), checker);
+ HttpMatcher m = new HttpMatcher(policyMappingConfig.authMechanism.orElse(null),
+ new HashSet<>(policyMappingConfig.methods.orElse(Collections.emptyList())), checker);
List perms = new ArrayList<>();
perms.add(m);
builder.addPath(path, perms);
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java
index f1996ea71553d5..18598de37f6405 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java
@@ -3,6 +3,7 @@
import java.util.Set;
import java.util.function.Function;
+import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
@@ -21,10 +22,17 @@ public interface HttpAuthenticationMechanism {
Uni getChallenge(RoutingContext context);
/**
- * Returns the required credential types. If there are no identity managers installed that support the
- * listed types then this mechanism will not be enabled.
+ * If this mechanism delegates authentication to the {@link IdentityProviderManager} using the
+ * {@link IdentityProviderManager#authenticate(AuthenticationRequest)} call, then the mechanism must provide
+ * supported {@link AuthenticationRequest} request types. It allows Quarkus to validate that one or more
+ * {@link IdentityProvider} providers with matching supported {@link IdentityProvider#getRequestType()} request
+ * types exist and fail otherwise.
+ *
+ * @return required credential types
*/
- Set> getCredentialTypes();
+ default Set> getCredentialTypes() {
+ return Set.of();
+ }
default Uni sendChallenge(RoutingContext context) {
return getChallenge(context).map(new ChallengeSender(context));
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java
index c7f6e9aa69af4a..d624696157fbe7 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java
@@ -30,6 +30,7 @@
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
+import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@@ -47,7 +48,7 @@ public class HttpAuthenticator {
public HttpAuthenticator(IdentityProviderManager identityProviderManager,
Event authFailureEvent,
Event authSuccessEvent,
- BeanManager beanManager,
+ BeanManager beanManager, HttpBuildTimeConfig httpBuildTimeConfig,
Instance httpAuthenticationMechanism,
Instance> providers,
@ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) {
@@ -56,6 +57,15 @@ public HttpAuthenticator(IdentityProviderManager identityProviderManager,
this.identityProviderManager = identityProviderManager;
List mechanisms = new ArrayList<>();
for (HttpAuthenticationMechanism mechanism : httpAuthenticationMechanism) {
+ if (mechanism.getCredentialTypes().isEmpty()) {
+ // mechanism does not require any IdentityProvider
+ log.debugf("HttpAuthenticationMechanism '%s' provided no required credential types, therefore it needs "
+ + "to be able to perform authentication without any IdentityProvider", mechanism.getClass().getName());
+ mechanisms.add(mechanism);
+ continue;
+ }
+
+ // mechanism requires an IdentityProvider, therefore we verify that such a provider exists
boolean found = false;
for (Class extends AuthenticationRequest> mechType : mechanism.getCredentialTypes()) {
for (IdentityProvider> i : providers) {
@@ -68,10 +78,22 @@ public HttpAuthenticator(IdentityProviderManager identityProviderManager,
break;
}
}
- // Add mechanism if there is a provider with matching credential type
- // If the mechanism has no credential types, just add it anyway
- if (found || mechanism.getCredentialTypes().isEmpty()) {
+ if (found) {
mechanisms.add(mechanism);
+ } else if (BasicAuthenticationMechanism.class.equals(mechanism.getClass())
+ && httpBuildTimeConfig.auth.basic.isEmpty()) {
+ log.debug("""
+ BasicAuthenticationMechanism has been enabled because no other authentication mechanism has been
+ detected, but there is no IdentityProvider based on username and password. Please use
+ one of supported extensions if you plan to use the mechanism.
+ For more information go to the https://quarkus.io/guides/security-basic-authentication-howto.
+ """);
+ } else {
+ throw new RuntimeException("""
+ HttpAuthenticationMechanism '%s' requires one or more IdentityProviders supporting at least one
+ of the following credentials types: %s.
+ Please refer to the https://quarkus.io/guides/security-identity-providers for more information.
+ """.formatted(mechanism.getClass().getName(), mechanism.getCredentialTypes()));
}
}
if (mechanisms.isEmpty()) {
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java
index f0c9062360111e..a522595a4cdadc 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java
@@ -1,8 +1,11 @@
package io.quarkus.vertx.http.runtime.security;
import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@@ -80,4 +83,28 @@ Uni runBlocking(RoutingContext context, Uni ident
}
+ class DefaultAuthorizationRequestContext implements AuthorizationRequestContext {
+ private final BlockingSecurityExecutor blockingExecutor;
+
+ public DefaultAuthorizationRequestContext(BlockingSecurityExecutor blockingExecutor) {
+ this.blockingExecutor = blockingExecutor;
+ }
+
+ @Override
+ public Uni runBlocking(RoutingContext context, Uni identityUni,
+ BiFunction function) {
+ return identityUni
+ .flatMap(new Function>() {
+ @Override
+ public Uni extends HttpSecurityPolicy.CheckResult> apply(SecurityIdentity identity) {
+ return blockingExecutor.executeBlocking(new Supplier() {
+ @Override
+ public HttpSecurityPolicy.CheckResult get() {
+ return function.apply(context, identity);
+ }
+ });
+ }
+ });
+ }
+ }
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java
index 45a2fcea2dac1e..25f3052d5bbcc0 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java
@@ -122,6 +122,14 @@ public static class ImmutablePathMatcherBuilder {
private static final String STRING_PATH_SEPARATOR = "/";
private final Map exactPathMatches = new HashMap<>();
+ /**
+ * Exact paths we proactively secure when more specify permissions are not specified.
+ * For example path for exact path '/api/hello' we add extra pattern for the '/api/hello/'.
+ * This helps to secure Jakarta REST endpoints by default as both paths may point to the same endpoint there.
+ * However, we only do that when user didn't declare any permission for the '/api/hello/'.
+ * This way, user can still forbid access to the `/api/hello' path and permit access to the '/api/hello/' path.
+ */
+ private final Map additionalExactPathMatches = new HashMap<>();
private final Map> pathsWithWildcard = new HashMap<>();
private BiConsumer handlerAccumulator;
@@ -188,6 +196,9 @@ public void accept(SubstringMatch match1, SubstringMatch match2) {
paths.put(p.path, handler, subPathMatcher);
}
+ for (var e : additionalExactPathMatches.entrySet()) {
+ exactPathMatches.putIfAbsent(e.getKey(), e.getValue());
+ }
int[] lengths = buildLengths(paths.keys());
return new ImmutablePathMatcher<>(defaultHandler, paths.asImmutableMap(), exactPathMatches, lengths,
hasPathWithInnerWildcard);
@@ -289,6 +300,20 @@ private void addExactPath(final String path, final T handler) {
} else {
exactPathMatches.put(path, handler);
}
+ // when 'path.equals("/api/hello")' then the other path is '/api/hello/'
+ final String otherPath;
+ if (path.endsWith(STRING_PATH_SEPARATOR)) {
+ if (path.length() == 1) {
+ // path '/' is only valid option, '' is not allowed
+ return;
+ }
+ // drop path separator
+ otherPath = path.substring(0, path.length() - 1);
+ } else {
+ otherPath = path + STRING_PATH_SEPARATOR;
+ }
+ // if key is already present, then we have the right handler into which new ones have already been merged
+ additionalExactPathMatches.putIfAbsent(otherPath, handler);
}
private static int[] buildLengths(Iterable keys) {
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java
index 037c3ceed32bb7..2127bc731d7ef9 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java
@@ -1,5 +1,7 @@
package io.quarkus.vertx.http.runtime.security;
+import static io.quarkus.vertx.http.runtime.PolicyMappingConfig.AppliesTo.ALL;
+
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;
@@ -18,7 +20,7 @@ public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchi
ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig,
ManagementInterfaceConfiguration runTimeConfig, Instance installedPolicies) {
- super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies);
+ super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies, ALL);
}
}
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 b258e4fa0be0ee..1138529568af54 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
@@ -1,5 +1,7 @@
package io.quarkus.vertx.http.runtime.security;
+import static io.quarkus.vertx.http.runtime.PolicyMappingConfig.AppliesTo.ALL;
+
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;
@@ -18,7 +20,7 @@ public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecu
PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig,
Instance installedPolicies) {
- super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies);
+ super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies, ALL);
}
}
diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java
index dfbfe2add67edf..5583cbf32dda78 100644
--- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java
+++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java
@@ -36,7 +36,9 @@ public void testPrefixPathWithEndingWildcard() {
final Object prefixPathMatcher2 = new Object();
matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1)
.addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2)
- .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build();
+ .addPath("/one/two/", prefixPathMatcher1).addPath("/one/two/three/", prefixPathMatcher2)
+ .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four/", prefixPathMatcher2)
+ .addPath("/one/two/three/four", exactPathMatcher3).build();
assertMatched(matcher, "/one/two/three", exactPathMatcher1);
assertMatched(matcher, "/one/two", exactPathMatcher2);
assertMatched(matcher, "/one/two/three/four", exactPathMatcher3);
@@ -72,8 +74,10 @@ public void testPrefixPathDefaultHandler() {
final Object prefixPathMatcher1 = new Object();
final Object prefixPathMatcher2 = new Object();
matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1).addPath("/*", defaultHandler)
- .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2)
- .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build();
+ .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two/three/", prefixPathMatcher2)
+ .addPath("/one/two", exactPathMatcher2).addPath("/one/two/", prefixPathMatcher1)
+ .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3)
+ .addPath("/one/two/three/four/", prefixPathMatcher2).build();
assertMatched(matcher, "/one/two/three", exactPathMatcher1);
assertMatched(matcher, "/one/two", exactPathMatcher2);
assertMatched(matcher, "/one/two/three/four", exactPathMatcher3);
@@ -120,7 +124,7 @@ public void testSpecialChars() {
ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2)
.addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3)
.addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).addPath("/*", HANDLER)
- .build();
+ .addPath("/one/two#three/", handler4).build();
assertMatched(matcher, "/one/two#three", handler2);
assertMatched(matcher, "/one/two?three=four", handler1);
assertMatched(matcher, "/one/any-value/three?one\\\\\\=two", handler3);
@@ -142,7 +146,7 @@ public void testSpecialChars() {
assertMatched(matcher, "/one1/two#three/christmas!", handler5);
assertMatched(matcher, "/one1/two#thre");
// no default handler
- matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2)
+ matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2).addPath("/one/two#three/", handler4)
.addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3)
.addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).build();
assertMatched(matcher, "/one/two#three", handler2);
@@ -181,7 +185,7 @@ public void testInnerWildcardsWithExactMatches() {
.addPath("/one/two/three", handler2).addPath("/one/two/three/four", handler3)
.addPath("/", handler4).addPath("/*", HANDLER).addPath("/one/two/*/four", handler5)
.addPath("/one/*/three/four", handler6).addPath("/*/two/three/four", handler7)
- .addPath("/*/two", handler8).build();
+ .addPath("/*/two", handler8).addPath("/*/two/three/four/", HANDLER).build();
assertMatched(matcher, "/one/two", handler1);
assertMatched(matcher, "/one/two/three", handler2);
assertMatched(matcher, "/one/two/three/four", handler3);
@@ -216,7 +220,7 @@ public void testInnerWildcardsOnly() {
assertMatched(matcher, "/one/two/three/4/five", handler5);
assertMatched(matcher, "/one/two/three/sergey/five", handler5);
assertMatched(matcher, "/one/two/three/sergey/five-ish");
- assertMatched(matcher, "/one/two/three/sergey/five/");
+ assertMatched(matcher, "/one/two/three/sergey/five/", handler5);
assertMatched(matcher, "/one/two/three/four", handler4);
assertMatched(matcher, "/one/two/3/four", handler4);
assertMatched(matcher, "/one/two/three", handler3);
@@ -228,11 +232,11 @@ public void testInnerWildcardsOnly() {
assertMatched(matcher, "/ho-hey/two", handler2);
assertMatched(matcher, "/ho-hey/two2");
assertMatched(matcher, "/ho-hey/two2/");
- assertMatched(matcher, "/ho-hey/two/");
+ assertMatched(matcher, "/ho-hey/two/", handler2);
assertMatched(matcher, "/ho-hey/hey-ho/three", handler1);
assertMatched(matcher, "/1/2/three", handler1);
assertMatched(matcher, "/1/two/three", handler1);
- assertMatched(matcher, "/1/two/three/");
+ assertMatched(matcher, "/1/two/three/", handler1);
assertMatched(matcher, "/1/two/three/f");
// no default path handler
matcher = ImmutablePathMatcher.builder().addPath("/*/two", handler2)
@@ -242,8 +246,8 @@ public void testInnerWildcardsOnly() {
assertMatched(matcher, "/one/two/three/four/five", handler5);
assertMatched(matcher, "/one/two/three/4/five", handler5);
assertMatched(matcher, "/one/two/three/sergey/five", handler5);
+ assertMatched(matcher, "/one/two/three/sergey/five/", handler5);
assertNotMatched(matcher, "/one/two/three/sergey/five-ish");
- assertNotMatched(matcher, "/one/two/three/sergey/five/");
assertMatched(matcher, "/one/two/three/four", handler4);
assertMatched(matcher, "/one/two/3/four", handler4);
assertMatched(matcher, "/one/two/three", handler3);
@@ -253,13 +257,13 @@ public void testInnerWildcardsOnly() {
assertMatched(matcher, "/two/two", handler2);
assertMatched(matcher, "/2/two", handler2);
assertMatched(matcher, "/ho-hey/two", handler2);
+ assertMatched(matcher, "/ho-hey/two/", handler2);
assertNotMatched(matcher, "/ho-hey/two2");
assertNotMatched(matcher, "/ho-hey/two2/");
- assertNotMatched(matcher, "/ho-hey/two/");
assertMatched(matcher, "/ho-hey/hey-ho/three", handler1);
assertMatched(matcher, "/1/2/three", handler1);
assertMatched(matcher, "/1/two/three", handler1);
- assertNotMatched(matcher, "/1/two/three/");
+ assertMatched(matcher, "/1/two/three/", handler1);
assertNotMatched(matcher, "/1/two/three/f");
}
@@ -377,7 +381,7 @@ public void testPrefixPathHandlerMerging() {
handler6.add("AgentBrown");
var matcher = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll).addPath("/path*", handler1)
.addPath("/path*", handler2).addPath("/path/*", handler3).addPath("/path/", handler4)
- .addPath("/path/*/", handler5).addPath("/*", handler6).build();
+ .addPath("/path/*/", handler5).addPath("/*", handler6).addPath("/path", handler1).build();
var handler = matcher.match("/path").getValue();
assertNotNull(handler);
assertTrue(handler.contains("Neo"));
@@ -505,11 +509,11 @@ public void testDefaultHandlerOneInnerWildcard() {
assertMatched(matcher, "/3/one");
assertMatched(matcher, "/4/one");
assertMatched(matcher, "/4/one");
+ assertMatched(matcher, "/1/one/");
assertNotMatched(matcher, "/");
assertNotMatched(matcher, "/1");
assertNotMatched(matcher, "/1/");
assertNotMatched(matcher, "/1/two");
- assertNotMatched(matcher, "/1/one/");
assertNotMatched(matcher, "/1/one1");
assertNotMatched(matcher, "/1/on");
assertNotMatched(matcher, "/1/one/two");
diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/options/TlsUtilsTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/options/TlsUtilsTest.java
new file mode 100644
index 00000000000000..802237f8cd7163
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/options/TlsUtilsTest.java
@@ -0,0 +1,76 @@
+package io.quarkus.vertx.http.runtime.options;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class TlsUtilsTest {
+
+ @ParameterizedTest
+ @CsvSource({
+ "server-keystore.jks, JKS, JKS",
+ "server-keystore.jks, jKs, JKS",
+ "server-keystore.jks, null, JKS",
+ "server-keystore.jks, PKCS12, PKCS12",
+ "server.keystore, null, null", // Failure expected
+ "server-keystore.p12, PKCS12, PKCS12",
+ "server-keystore.p12, pKCs12, PKCS12",
+ "server-keystore.p12, null, PKCS12",
+ "server-keystore.pfx, null, PKCS12",
+ "server-keystore.pkcs12, null, PKCS12",
+ "server-keystore.pkcs12, JKS, JKS",
+ "server.keystore.key, null, PEM",
+ "server.keystore.crt, null, PEM",
+ "server.keystore.pem, null, PEM",
+ "server.keystore.key, JKS, JKS",
+ "server.keystore.pom, PeM, PEM",
+ })
+ void testKeyStoreTypeDetection(String file, String userType, String expectedType) {
+ Path path = new File("target/certs/" + file).toPath();
+ Optional type = Optional.ofNullable(userType.equals("null") ? null : userType);
+ if (expectedType.equals("null")) {
+ String message = assertThrows(IllegalArgumentException.class, () -> TlsUtils.getKeyStoreType(path, type))
+ .getMessage();
+ assertTrue(message.contains("keystore"));
+ } else {
+ assertEquals(expectedType.toLowerCase(), TlsUtils.getKeyStoreType(path, type));
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "server-truststore.jks, JKS, JKS",
+ "server-truststore.jks, jKs, JKS",
+ "server-truststore.jks, null, JKS",
+ "server-truststore.jks, PKCS12, PKCS12",
+ "server.truststore, null, null", // Failure expected
+ "server-truststore.p12, PKCS12, PKCS12",
+ "server-truststore.p12, pKCs12, PKCS12",
+ "server-truststore.p12, null, PKCS12",
+ "server-truststore.pfx, null, PKCS12",
+ "server-truststore.pkcs12, null, PKCS12",
+ "server-truststore.pkcs12, JKS, JKS",
+ "server.truststore.key, null, PEM",
+ "server.truststore.crt, null, PEM",
+ "server.truststore.pem, null, PEM",
+ "server.truststore.key, JKS, JKS",
+ "server.truststore.pom, PeM, PEM",
+ })
+ void testTrustStoreTypeDetection(String file, String userType, String expectedType) {
+ Path path = new File("target/certs/" + file).toPath();
+ Optional type = Optional.ofNullable(userType.equals("null") ? null : userType);
+ if (expectedType.equals("null")) {
+ String message = assertThrows(IllegalArgumentException.class, () -> TlsUtils.getTruststoreType(path, type))
+ .getMessage();
+ assertTrue(message.contains("truststore"));
+ } else {
+ assertEquals(expectedType.toLowerCase(), TlsUtils.getTruststoreType(path, type));
+ }
+ }
+
+}
diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/SSLConfigHelper.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/SSLConfigHelper.java
index e7d77a2ca31581..1ca2f41b995dcd 100644
--- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/SSLConfigHelper.java
+++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/SSLConfigHelper.java
@@ -14,7 +14,7 @@
public class SSLConfigHelper {
public static void configurePemTrustOptions(TCPSSLOptions options, PemTrustCertConfiguration configuration) {
- if (configuration.enabled()) {
+ if (configuration.enabled() || (configuration.certs().isPresent() && !configuration.certs().get().isEmpty())) {
ensureTrustOptionsNotSet(options);
options.setTrustOptions(toPemTrustOptions(configuration));
}
diff --git a/extensions/virtual-threads/runtime/src/main/java/io/quarkus/virtual/threads/ContextPreservingExecutorService.java b/extensions/virtual-threads/runtime/src/main/java/io/quarkus/virtual/threads/ContextPreservingExecutorService.java
index 09499bb96f67f5..50e8b5e0427ab1 100644
--- a/extensions/virtual-threads/runtime/src/main/java/io/quarkus/virtual/threads/ContextPreservingExecutorService.java
+++ b/extensions/virtual-threads/runtime/src/main/java/io/quarkus/virtual/threads/ContextPreservingExecutorService.java
@@ -2,13 +2,17 @@
import java.util.Collection;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+import io.vertx.core.Context;
import io.vertx.core.Vertx;
import io.vertx.core.impl.ContextInternal;
@@ -22,26 +26,77 @@ class ContextPreservingExecutorService implements ExecutorService {
this.delegate = delegate;
}
- public void execute(final Runnable command) {
- var context = Vertx.currentContext();
- if (!(context instanceof ContextInternal)) {
- delegate.execute(command);
- } else {
- ContextInternal contextInternal = (ContextInternal) context;
- delegate.execute(new Runnable() {
- @Override
- public void run() {
- final var previousContext = contextInternal.beginDispatch();
- try {
- command.run();
- } finally {
- contextInternal.endDispatch(previousContext);
- }
+ private static final class ContextPreservingRunnable implements Runnable {
+
+ private final Runnable task;
+ private final Context context;
+
+ public ContextPreservingRunnable(Runnable task) {
+ this.task = task;
+ this.context = Vertx.currentContext();
+ }
+
+ @Override
+ public void run() {
+ if (context instanceof ContextInternal) {
+ ContextInternal contextInternal = (ContextInternal) context;
+ final var previousContext = contextInternal.beginDispatch();
+ try {
+ task.run();
+ } finally {
+ contextInternal.endDispatch(previousContext);
+ }
+ } else {
+ task.run();
+ }
+ }
+ }
+
+ private static final class ContextPreservingCallable implements Callable {
+
+ private final Callable task;
+ private final Context context;
+
+ public ContextPreservingCallable(Callable task) {
+ this.task = task;
+ this.context = Vertx.currentContext();
+ }
+
+ @Override
+ public T call() throws Exception {
+ if (context instanceof ContextInternal) {
+ ContextInternal contextInternal = (ContextInternal) context;
+ final var previousContext = contextInternal.beginDispatch();
+ try {
+ return task.call();
+ } finally {
+ contextInternal.endDispatch(previousContext);
}
- });
+ } else {
+ return task.call();
+ }
}
}
+ private static Runnable decorate(Runnable command) {
+ Objects.requireNonNull(command);
+ return new ContextPreservingRunnable(command);
+ }
+
+ private static Callable decorate(Callable task) {
+ Objects.requireNonNull(task);
+ return new ContextPreservingCallable<>(task);
+ }
+
+ private static Collection extends Callable> decorateAll(Collection extends Callable> tasks) {
+ Objects.requireNonNull(tasks);
+ return tasks.stream().map(ContextPreservingExecutorService::decorate).collect(Collectors.toList());
+ }
+
+ public void execute(final Runnable command) {
+ delegate.execute(decorate(command));
+ }
+
public boolean isShutdown() {
return delegate.isShutdown();
}
@@ -56,39 +111,39 @@ public boolean awaitTermination(final long timeout, final TimeUnit unit) throws
@Override
public Future submit(Callable task) {
- return delegate.submit(task);
+ return delegate.submit(decorate(task));
}
@Override
public Future submit(Runnable task, T result) {
- return delegate.submit(task, result);
+ return submit(Executors.callable(task, result));
}
@Override
public Future> submit(Runnable task) {
- return delegate.submit(task);
+ return delegate.submit(decorate(task));
}
@Override
public List> invokeAll(Collection extends Callable> tasks) throws InterruptedException {
- return delegate.invokeAll(tasks);
+ return delegate.invokeAll(decorateAll(tasks));
}
@Override
public List> invokeAll(Collection extends Callable> tasks, long timeout, TimeUnit unit)
throws InterruptedException {
- return delegate.invokeAll(tasks, timeout, unit);
+ return delegate.invokeAll(decorateAll(tasks), timeout, unit);
}
@Override
public T invokeAny(Collection extends Callable> tasks) throws InterruptedException, ExecutionException {
- return delegate.invokeAny(tasks);
+ return delegate.invokeAny(decorateAll(tasks));
}
@Override
public T invokeAny(Collection extends Callable> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
- return delegate.invokeAny(tasks, timeout, unit);
+ return delegate.invokeAny(decorateAll(tasks), timeout, unit);
}
public void shutdown() {
diff --git a/extensions/virtual-threads/runtime/src/test/java/io/quarkus/virtual/threads/VirtualThreadExecutorSupplierTest.java b/extensions/virtual-threads/runtime/src/test/java/io/quarkus/virtual/threads/VirtualThreadExecutorSupplierTest.java
index 04cc1219d35a6a..74b5a49c48fab0 100644
--- a/extensions/virtual-threads/runtime/src/test/java/io/quarkus/virtual/threads/VirtualThreadExecutorSupplierTest.java
+++ b/extensions/virtual-threads/runtime/src/test/java/io/quarkus/virtual/threads/VirtualThreadExecutorSupplierTest.java
@@ -5,17 +5,34 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
+import io.vertx.core.Context;
+import io.vertx.core.Vertx;
class VirtualThreadExecutorSupplierTest {
+ @BeforeEach
+ void configRecorder() {
+ VirtualThreadsRecorder.config = new VirtualThreadsConfig();
+ VirtualThreadsRecorder.config.enabled = true;
+ VirtualThreadsRecorder.config.namePrefix = Optional.empty();
+ }
+
@Test
@EnabledForJreRange(min = JRE.JAVA_20, disabledReason = "Virtual Threads are a preview feature starting from Java 20")
void virtualThreadCustomScheduler()
@@ -44,6 +61,74 @@ void execute() throws ClassNotFoundException, InvocationTargetException, NoSuchM
assertSubscriber.awaitItem(Duration.ofSeconds(1)).assertCompleted();
}
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_20, disabledReason = "Virtual Threads are a preview feature starting from Java 20")
+ void executePropagatesVertxContext() throws ExecutionException, InterruptedException {
+ ExecutorService executorService = VirtualThreadsRecorder.getCurrent();
+ Vertx vertx = Vertx.vertx();
+ CompletableFuture future = new CompletableFuture<>();
+ vertx.executeBlocking(() -> {
+ executorService.execute(() -> {
+ assertThatItRunsOnVirtualThread();
+ future.complete(Vertx.currentContext());
+ });
+ return null;
+ }).toCompletionStage().toCompletableFuture().get();
+ assertThat(future.get()).isNotNull();
+ }
+
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_20, disabledReason = "Virtual Threads are a preview feature starting from Java 20")
+ void executePropagatesVertxContextMutiny() {
+ ExecutorService executorService = VirtualThreadsRecorder.getCurrent();
+ Vertx vertx = Vertx.vertx();
+ var assertSubscriber = Uni.createFrom().voidItem()
+ .runSubscriptionOn(command -> vertx.executeBlocking(() -> {
+ command.run();
+ return null;
+ }))
+ .emitOn(executorService)
+ .map(x -> {
+ assertThatItRunsOnVirtualThread();
+ return Vertx.currentContext();
+ })
+ .subscribe().withSubscriber(UniAssertSubscriber.create());
+ assertThat(assertSubscriber.awaitItem().assertCompleted().getItem()).isNotNull();
+ }
+
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_20, disabledReason = "Virtual Threads are a preview feature starting from Java 20")
+ void submitPropagatesVertxContext() throws ExecutionException, InterruptedException {
+ ExecutorService executorService = VirtualThreadsRecorder.getCurrent();
+ Vertx vertx = Vertx.vertx();
+ CompletableFuture future = new CompletableFuture<>();
+ vertx.executeBlocking(() -> {
+ executorService.submit(() -> {
+ assertThatItRunsOnVirtualThread();
+ future.complete(Vertx.currentContext());
+ });
+ return null;
+ }).toCompletionStage().toCompletableFuture().get();
+ assertThat(future.get()).isNotNull();
+ }
+
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_20, disabledReason = "Virtual Threads are a preview feature starting from Java 20")
+ void invokeAllPropagatesVertxContext() throws ExecutionException, InterruptedException {
+ ExecutorService executorService = VirtualThreadsRecorder.getCurrent();
+ Vertx vertx = Vertx.vertx();
+ List> futures = vertx.executeBlocking(() -> {
+ return executorService.invokeAll(List.of((Callable) () -> {
+ assertThatItRunsOnVirtualThread();
+ return Vertx.currentContext();
+ }, (Callable) () -> {
+ assertThatItRunsOnVirtualThread();
+ return Vertx.currentContext();
+ }));
+ }).toCompletionStage().toCompletableFuture().get();
+ assertThat(futures).allSatisfy(contextFuture -> assertThat(contextFuture.get()).isNotNull());
+ }
+
public static void assertThatItRunsOnVirtualThread() {
// We cannot depend on a Java 20.
try {
diff --git a/extensions/websockets-next/pom.xml b/extensions/websockets-next/pom.xml
new file mode 100644
index 00000000000000..6eb24958661858
--- /dev/null
+++ b/extensions/websockets-next/pom.xml
@@ -0,0 +1,20 @@
+
+
+
+ quarkus-extensions-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ quarkus-websockets-next-aggregator
+ Quarkus - WebSockets Next Aggregator
+ pom
+
+ server
+
+
+
diff --git a/extensions/websockets-next/server/deployment/pom.xml b/extensions/websockets-next/server/deployment/pom.xml
new file mode 100644
index 00000000000000..f2405b4fc35c90
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ quarkus-websockets-next-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+ 4.0.0
+
+ quarkus-websockets-next-deployment
+ Quarkus - WebSockets Next - Deployment
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkus
+ quarkus-vertx-http-deployment
+
+
+ io.quarkus
+ quarkus-jackson-deployment
+
+
+ io.quarkus
+ quarkus-websockets-next
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+ maven-surefire-plugin
+
+
+
+ org.jboss.logmanager.LogManager
+ INFO
+ ${maven.home}
+
+
+
+
+
+
+
diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/GeneratedEndpointBuildItem.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/GeneratedEndpointBuildItem.java
new file mode 100644
index 00000000000000..7e4d72cc1b6d1d
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/GeneratedEndpointBuildItem.java
@@ -0,0 +1,18 @@
+package io.quarkus.websockets.next.deployment;
+
+import io.quarkus.builder.item.MultiBuildItem;
+
+/**
+ * A generated {@link io.quarkus.websockets.next.runtime.WebSocketEndpoint}.
+ */
+final class GeneratedEndpointBuildItem extends MultiBuildItem {
+
+ final String className;
+ final String path;
+
+ GeneratedEndpointBuildItem(String className, String path) {
+ this.className = className;
+ this.path = path;
+ }
+
+}
diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java
new file mode 100644
index 00000000000000..7677d3057782f4
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java
@@ -0,0 +1,38 @@
+package io.quarkus.websockets.next.deployment;
+
+import org.jboss.jandex.DotName;
+
+import io.quarkus.websockets.next.BinaryMessage;
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnMessage;
+import io.quarkus.websockets.next.OnOpen;
+import io.quarkus.websockets.next.TextMessage;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+import io.smallrye.common.annotation.Blocking;
+import io.smallrye.common.annotation.RunOnVirtualThread;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+final class WebSocketDotNames {
+
+ static final DotName WEB_SOCKET = DotName.createSimple(WebSocket.class);
+ static final DotName WEB_SOCKET_CONNECTION = DotName.createSimple(WebSocketConnection.class);
+ static final DotName ON_OPEN = DotName.createSimple(OnOpen.class);
+ static final DotName ON_MESSAGE = DotName.createSimple(OnMessage.class);
+ static final DotName ON_CLOSE = DotName.createSimple(OnClose.class);
+ static final DotName UNI = DotName.createSimple(Uni.class);
+ static final DotName MULTI = DotName.createSimple(Multi.class);
+ static final DotName RUN_ON_VIRTUAL_THREAD = DotName.createSimple(RunOnVirtualThread.class);
+ static final DotName BLOCKING = DotName.createSimple(Blocking.class);
+ static final DotName STRING = DotName.createSimple(String.class);
+ static final DotName BUFFER = DotName.createSimple(Buffer.class);
+ static final DotName JSON_OBJECT = DotName.createSimple(JsonObject.class);
+ static final DotName JSON_ARRAY = DotName.createSimple(JsonArray.class);
+ static final DotName VOID = DotName.createSimple(Void.class);
+ static final DotName BINARY_MESSAGE = DotName.createSimple(BinaryMessage.class);
+ static final DotName TEXT_MESSAGE = DotName.createSimple(TextMessage.class);
+}
diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketEndpointBuildItem.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketEndpointBuildItem.java
new file mode 100644
index 00000000000000..1c1fcb112f22ca
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketEndpointBuildItem.java
@@ -0,0 +1,153 @@
+package io.quarkus.websockets.next.deployment;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.PrimitiveType;
+import org.jboss.jandex.Type;
+import org.jboss.jandex.Type.Kind;
+
+import io.quarkus.arc.processor.BeanInfo;
+import io.quarkus.builder.item.MultiBuildItem;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.WebSocketEndpoint.ExecutionModel;
+import io.quarkus.websockets.next.runtime.WebSocketEndpoint.MessageType;
+
+/**
+ * This build item represents a WebSocket endpoint class.
+ */
+public final class WebSocketEndpointBuildItem extends MultiBuildItem {
+
+ public final BeanInfo bean;
+ public final String path;
+ public final WebSocket.ExecutionMode executionMode;
+ public final Callback onOpen;
+ public final Callback onMessage;
+ public final Callback onClose;
+
+ public WebSocketEndpointBuildItem(BeanInfo bean, String path, WebSocket.ExecutionMode executionMode, Callback onOpen,
+ Callback onMessage, Callback onClose) {
+ this.bean = bean;
+ this.path = path;
+ this.executionMode = executionMode;
+ this.onOpen = onOpen;
+ this.onMessage = onMessage;
+ this.onClose = onClose;
+ }
+
+ public static class Callback {
+
+ public final AnnotationInstance annotation;
+ public final MethodInfo method;
+ public final ExecutionModel executionModel;
+ public final MessageType consumedMessageType;
+ public final MessageType producedMessageType;
+
+ public Callback(AnnotationInstance annotation, MethodInfo method, ExecutionModel executionModel) {
+ this.method = method;
+ this.annotation = annotation;
+ this.executionModel = executionModel;
+ this.consumedMessageType = initMessageType(method.parameters().isEmpty() ? null : method.parameterType(0));
+ this.producedMessageType = initMessageType(method.returnType());
+ }
+
+ public Type returnType() {
+ return method.returnType();
+ }
+
+ public Type messageParamType() {
+ return acceptsMessage() ? method.parameterType(0) : null;
+ }
+
+ public boolean isReturnTypeVoid() {
+ return returnType().kind() == Kind.VOID;
+ }
+
+ public boolean isReturnTypeUni() {
+ return WebSocketDotNames.UNI.equals(returnType().name());
+ }
+
+ public boolean isReturnTypeMulti() {
+ return WebSocketDotNames.MULTI.equals(returnType().name());
+ }
+
+ public boolean acceptsMessage() {
+ return consumedMessageType != MessageType.NONE;
+ }
+
+ public boolean acceptsBinaryMessage() {
+ return consumedMessageType == MessageType.BINARY;
+ }
+
+ public boolean acceptsMulti() {
+ return acceptsMessage() && method.parameterType(0).name().equals(WebSocketDotNames.MULTI);
+ }
+
+ public WebSocketEndpoint.MessageType consumedMessageType() {
+ return consumedMessageType;
+ }
+
+ public WebSocketEndpoint.MessageType producedMessageType() {
+ return producedMessageType;
+ }
+
+ public boolean broadcast() {
+ AnnotationValue broadcastValue = annotation.value("broadcast");
+ return broadcastValue != null && broadcastValue.asBoolean();
+ }
+
+ public DotName getInputCodec() {
+ return getCodec("inputCodec");
+ }
+
+ public DotName getOutputCodec() {
+ DotName output = getCodec("outputCodec");
+ return output != null ? output : getInputCodec();
+ }
+
+ private DotName getCodec(String valueName) {
+ AnnotationInstance messageAnnotation = method.declaredAnnotation(WebSocketDotNames.BINARY_MESSAGE);
+ if (messageAnnotation == null) {
+ messageAnnotation = method.declaredAnnotation(WebSocketDotNames.TEXT_MESSAGE);
+ }
+ if (messageAnnotation != null) {
+ AnnotationValue codecValue = messageAnnotation.value(valueName);
+ if (codecValue != null) {
+ return codecValue.asClass().name();
+ }
+ }
+ return null;
+ }
+
+ MessageType initMessageType(Type messageType) {
+ MessageType ret = MessageType.NONE;
+ if (messageType != null && !messageType.name().equals(WebSocketDotNames.VOID)) {
+ if (method.hasDeclaredAnnotation(WebSocketDotNames.BINARY_MESSAGE)) {
+ ret = MessageType.BINARY;
+ } else if (method.hasDeclaredAnnotation(WebSocketDotNames.TEXT_MESSAGE)) {
+ ret = MessageType.TEXT;
+ } else {
+ if (isByteArray(messageType) || WebSocketDotNames.BUFFER.equals(messageType.name())) {
+ ret = MessageType.BINARY;
+ } else {
+ ret = MessageType.TEXT;
+ }
+ }
+ }
+ return ret;
+ }
+
+ static boolean isByteArray(Type type) {
+ return type.kind() == Kind.ARRAY && PrimitiveType.BYTE.equals(type.asArrayType().constituent());
+ }
+
+ static boolean isUniVoid(Type type) {
+ return WebSocketDotNames.UNI.equals(type.name())
+ && type.asParameterizedType().arguments().get(0).name().equals(WebSocketDotNames.VOID);
+ }
+
+ }
+
+}
diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java
new file mode 100644
index 00000000000000..5831efff6c734e
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java
@@ -0,0 +1,743 @@
+package io.quarkus.websockets.next.deployment;
+
+import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import jakarta.enterprise.context.SessionScoped;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.ClassInfo.NestingType;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.Type;
+import org.jboss.jandex.Type.Kind;
+
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
+import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
+import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
+import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem;
+import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem.ContextConfiguratorBuildItem;
+import io.quarkus.arc.deployment.CustomScopeBuildItem;
+import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
+import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
+import io.quarkus.arc.processor.BeanInfo;
+import io.quarkus.arc.processor.DotNames;
+import io.quarkus.arc.processor.Types;
+import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.gizmo.BytecodeCreator;
+import io.quarkus.gizmo.ClassCreator;
+import io.quarkus.gizmo.ClassOutput;
+import io.quarkus.gizmo.FunctionCreator;
+import io.quarkus.gizmo.MethodCreator;
+import io.quarkus.gizmo.MethodDescriptor;
+import io.quarkus.gizmo.ResultHandle;
+import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
+import io.quarkus.vertx.http.deployment.RouteBuildItem;
+import io.quarkus.vertx.http.runtime.HandlerType;
+import io.quarkus.websockets.next.TextMessageCodec;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+import io.quarkus.websockets.next.WebSocketServerException;
+import io.quarkus.websockets.next.WebSocketsRuntimeConfig;
+import io.quarkus.websockets.next.deployment.WebSocketEndpointBuildItem.Callback;
+import io.quarkus.websockets.next.runtime.Codecs;
+import io.quarkus.websockets.next.runtime.ConnectionManager;
+import io.quarkus.websockets.next.runtime.ContextSupport;
+import io.quarkus.websockets.next.runtime.JsonTextMessageCodec;
+import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.WebSocketEndpoint.ExecutionModel;
+import io.quarkus.websockets.next.runtime.WebSocketEndpoint.MessageType;
+import io.quarkus.websockets.next.runtime.WebSocketEndpointBase;
+import io.quarkus.websockets.next.runtime.WebSocketServerRecorder;
+import io.quarkus.websockets.next.runtime.WebSocketSessionContext;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+import io.smallrye.mutiny.groups.UniCreate;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+public class WebSocketServerProcessor {
+
+ static final String ENDPOINT_SUFFIX = "_WebSocketEndpoint";
+ static final String NESTED_SEPARATOR = "$_";
+
+ private static final Pattern PATH_PARAM_PATTERN = Pattern.compile("\\{[a-zA-Z0-9_]+\\}");
+
+ @BuildStep
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem("websockets-next");
+ }
+
+ @BuildStep
+ BeanDefiningAnnotationBuildItem beanDefiningAnnotation() {
+ return new BeanDefiningAnnotationBuildItem(WebSocketDotNames.WEB_SOCKET, DotNames.SINGLETON);
+ }
+
+ @BuildStep
+ void unremovableBeans(BuildProducer unremovableBeans) {
+ unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(TextMessageCodec.class));
+ }
+
+ @BuildStep
+ public void collectEndpoints(BeanArchiveIndexBuildItem beanArchiveIndex,
+ BeanDiscoveryFinishedBuildItem beanDiscoveryFinished,
+ BuildProducer endpoints) {
+
+ IndexView index = beanArchiveIndex.getIndex();
+ Map pathToEndpoint = new HashMap<>();
+
+ for (BeanInfo bean : beanDiscoveryFinished.beanStream().classBeans()) {
+ ClassInfo beanClass = bean.getTarget().get().asClass();
+ AnnotationInstance webSocketAnnotation = beanClass.annotation(WebSocketDotNames.WEB_SOCKET);
+ if (webSocketAnnotation != null) {
+ String path = getPath(webSocketAnnotation.value("path").asString());
+ if (beanClass.nestingType() == NestingType.INNER) {
+ // Sub-websocket - merge the path from the enclosing classes
+ path = mergePath(getPathPrefix(index, beanClass.enclosingClass()), path);
+ }
+ DotName previous = pathToEndpoint.put(path, beanClass.name());
+ if (previous != null) {
+ throw new WebSocketServerException(
+ String.format("Multiple endpoints [%s, %s] define the same path: %s", previous, beanClass, path));
+ }
+ Callback onOpen = findCallback(beanArchiveIndex.getIndex(), beanClass, WebSocketDotNames.ON_OPEN,
+ this::validateOnOpen);
+ Callback onMessage = findCallback(beanArchiveIndex.getIndex(), beanClass, WebSocketDotNames.ON_MESSAGE,
+ this::validateOnMessage);
+ Callback onClose = findCallback(beanArchiveIndex.getIndex(), beanClass, WebSocketDotNames.ON_CLOSE,
+ this::validateOnClose);
+ if (onOpen == null && onMessage == null) {
+ throw new WebSocketServerException(
+ "The endpoint must declare at least one method annotated with @OnMessage or @OnOpen: " + beanClass);
+ }
+ AnnotationValue executionMode = webSocketAnnotation.value("executionMode");
+ endpoints.produce(new WebSocketEndpointBuildItem(bean, path,
+ executionMode != null ? WebSocket.ExecutionMode.valueOf(executionMode.asEnum())
+ : WebSocket.ExecutionMode.SERIAL,
+ onOpen,
+ onMessage, onClose));
+ }
+ }
+ }
+
+ @BuildStep
+ public void generateEndpoints(List endpoints,
+ BuildProducer generatedClasses,
+ BuildProducer generatedEndpoints,
+ BuildProducer reflectiveClasses) {
+ ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function() {
+ @Override
+ public String apply(String name) {
+ int idx = name.indexOf(ENDPOINT_SUFFIX);
+ if (idx != -1) {
+ name = name.substring(0, idx);
+ }
+ if (name.contains(NESTED_SEPARATOR)) {
+ name = name.replace(NESTED_SEPARATOR, "$");
+ }
+ return name;
+ }
+ });
+ for (WebSocketEndpointBuildItem endpoint : endpoints) {
+ // For each WebSocket endpoint bean generate an implementation of WebSocketEndpoint
+ // A new instance of this generated endpoint is created for each client connection
+ // The generated endpoint ensures the correct execution model is used
+ // and delegates callback invocations to the endpoint bean
+ String generatedName = generateEndpoint(endpoint, classOutput);
+ reflectiveClasses.produce(ReflectiveClassBuildItem.builder(generatedName).constructors().build());
+ generatedEndpoints.produce(new GeneratedEndpointBuildItem(generatedName, endpoint.path));
+ }
+ }
+
+ @Record(RUNTIME_INIT)
+ @BuildStep
+ public void registerRoutes(WebSocketServerRecorder recorder, HttpRootPathBuildItem httpRootPath,
+ List generatedEndpoints,
+ BuildProducer routes) {
+
+ for (GeneratedEndpointBuildItem endpoint : generatedEndpoints) {
+ RouteBuildItem.Builder builder = RouteBuildItem.builder()
+ .route(httpRootPath.relativePath(endpoint.path))
+ .handlerType(HandlerType.NORMAL)
+ .handler(recorder.createEndpointHandler(endpoint.className));
+ routes.produce(builder.build());
+ }
+ }
+
+ @BuildStep
+ AdditionalBeanBuildItem additionalBeans() {
+ return AdditionalBeanBuildItem.builder().setUnremovable()
+ .addBeanClasses(Codecs.class, JsonTextMessageCodec.class, ConnectionManager.class).build();
+ }
+
+ @BuildStep
+ @Record(RUNTIME_INIT)
+ void syntheticBeans(WebSocketServerRecorder recorder, BuildProducer syntheticBeans) {
+ syntheticBeans.produce(SyntheticBeanBuildItem.configure(WebSocketConnection.class)
+ .scope(SessionScoped.class)
+ .setRuntimeInit()
+ .supplier(recorder.connectionSupplier())
+ .unremovable()
+ .done());
+ }
+
+ @BuildStep
+ ContextConfiguratorBuildItem registerSessionContext(ContextRegistrationPhaseBuildItem phase) {
+ return new ContextConfiguratorBuildItem(phase.getContext()
+ .configure(SessionScoped.class)
+ .normal()
+ .contextClass(WebSocketSessionContext.class));
+ }
+
+ @BuildStep
+ CustomScopeBuildItem registerSessionScope() {
+ return new CustomScopeBuildItem(DotName.createSimple(SessionScoped.class.getName()));
+ }
+
+ static String mergePath(String prefix, String path) {
+ if (prefix.endsWith("/")) {
+ prefix = prefix.substring(0, prefix.length() - 1);
+ }
+ if (!path.startsWith("/")) {
+ path = "/" + path;
+ }
+ return prefix + path;
+ }
+
+ static String getPath(String path) {
+ StringBuilder sb = new StringBuilder();
+ Matcher m = PATH_PARAM_PATTERN.matcher(path);
+ while (m.find()) {
+ // Replace {foo} with :foo
+ String match = m.group();
+ m.appendReplacement(sb, ":" + match.subSequence(1, match.length() - 1));
+ }
+ m.appendTail(sb);
+ return sb.toString();
+ }
+
+ private void validateCallback(MethodInfo callback) {
+ if (callback.hasDeclaredAnnotation(WebSocketDotNames.BINARY_MESSAGE)
+ && callback.hasDeclaredAnnotation(WebSocketDotNames.TEXT_MESSAGE)) {
+ throw new WebSocketServerException(
+ "Either @BinaryMessage or @TextMessage can be declared on a callback: " + callbackToString(callback));
+ }
+ }
+
+ private void validateOnMessage(MethodInfo callback) {
+ if (callback.parameters().size() != 1) {
+ throw new WebSocketServerException(
+ "@OnMessage callback must accept exactly one parameter: " + callbackToString(callback));
+ }
+ }
+
+ private String callbackToString(MethodInfo callback) {
+ return callback.declaringClass().name() + "#" + callback.name() + "()";
+ }
+
+ private String getPathPrefix(IndexView index, DotName enclosingClassName) {
+ ClassInfo enclosingClass = index.getClassByName(enclosingClassName);
+ if (enclosingClass == null) {
+ throw new WebSocketServerException("Enclosing class not found in index: " + enclosingClass);
+ }
+ AnnotationInstance webSocketAnnotation = enclosingClass.annotation(WebSocketDotNames.WEB_SOCKET);
+ if (webSocketAnnotation != null) {
+ String path = getPath(webSocketAnnotation.value("path").asString());
+ if (enclosingClass.nestingType() == NestingType.INNER) {
+ return mergePath(getPathPrefix(index, enclosingClass.enclosingClass()), path);
+ } else {
+ return path.endsWith("/") ? path.substring(path.length() - 1) : path;
+ }
+ }
+ return "";
+ }
+
+ private void validateOnOpen(MethodInfo callback) {
+ if (!callback.parameters().isEmpty()) {
+ throw new WebSocketServerException(
+ "@OnOpen callback must not accept any parameters: " + callbackToString(callback));
+ }
+ }
+
+ private void validateOnClose(MethodInfo callback) {
+ if (callback.returnType().kind() != Kind.VOID && !Callback.isUniVoid(callback.returnType())) {
+ throw new WebSocketServerException(
+ "@OnClose callback must return void or Uni: " + callbackToString(callback));
+ }
+ if (!callback.parameters().isEmpty()) {
+ throw new WebSocketServerException(
+ "@OnClose callback must not accept any parameters: " + callbackToString(callback));
+ }
+ }
+
+ /**
+ * The generated endpoint class looks like:
+ *
+ *
+ * public class Echo_WebSocketEndpoint extends WebSocketEndpointBase {
+ *
+ * public WebSocket.ExecutionMode executionMode() {
+ * return WebSocket.ExecutionMode.SERIAL;
+ * }
+ *
+ * public Echo_WebSocketEndpoint(WebSocketConnection connection, Codecs codecs,
+ * WebSocketRuntimeConfig config, ContextSupport contextSupport) {
+ * super(context, connection, codecs, config, contextActivator);
+ * }
+ *
+ * public WebSocketEndpoint.MessageType consumedMessageType() {
+ * return MessageType.TEXT;
+ * }
+ *
+ * public Uni doOnMessage(Object message) {
+ * Uni uni = ((Echo) super.beanInstance("MTd91f3oxHtG8gnznR7XcZBCLdE")).echo((String) message);
+ * if (uni != null) {
+ * // The lambda is implemented as a generated function: Echo_WebSocketEndpoint$$function$$1
+ * return uni.chain(m -> sendText(m, false));
+ * } else {
+ * return Uni.createFrom().voidItem();
+ * }
+ * }
+ *
+ * public WebSocketEndpoint.ExecutionModel onMessageExecutionModel() {
+ * return ExecutionModel.EVENT_LOOP;
+ * }
+ * }
+ *