diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 79d38079449a5b..5dfccc79b00be7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,6 +8,11 @@ body: **To report a Quarkus security vulnerability, please [send an email to `security@quarkus.io`](mailto:security@quarkus.io) with all the details.** :warning: Do **NOT** create a public issue on GitHub for security vulnerabilities. See our [security policy](https://github.com/quarkusio/quarkus/security/policy) for more details. :warning: + - type: markdown + attributes: + value: | + To maximize the chance of your issue being handled in a timely manner, please ensure you have tested that the issue occurs on the absolute latest version of Quarkus, or latest LTS version. + Moreover, providing a minimal canonical sample project that makes it easy to reproduce the issue is a huge help to the community when it comes to understanding, debugging, fixing the problem and testing the fix. - type: textarea id: description validations: diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md index b8815ccbc6ce1f..01376409382993 100644 --- a/.github/ISSUE_TEMPLATE/epic.md +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -8,9 +8,11 @@ assignees: '' --- ### Description + (A high level description of the work) ### Analysis + (links to analysis docs containing architecture design work, requirements gathering, etc) - body-include: '' - number: ${{ github.event.number }} \ No newline at end of file + body-marker: diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index c10cebf499923b..4272c55f44f1c2 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -6,9 +6,18 @@ on: types: - completed +defaults: + run: + shell: bash + jobs: preview: runs-on: ubuntu-latest + permissions: + actions: read + issues: write + # this is unfortunately needed to be able to write comments on pull requests + pull-requests: write if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - uses: actions/checkout@v4 @@ -16,6 +25,10 @@ jobs: repository: quarkusio/quarkusio.github.io fetch-depth: 5000 fetch-tags: false + - uses: actions/checkout@v4 + with: + repository: quarkusio/quarkus + path: quarkus-main - name: Install git-restore-time run: sudo apt-get install -y git-restore-mtime @@ -23,25 +36,33 @@ jobs: - name: Restore mtime run: git restore-mtime - # There is a weird issue with download-artifact@v4 - # keeping the external action for now - name: Download PR Artifact - uses: dawidd6/action-download-artifact@v3 + uses: actions/download-artifact@v4 with: - workflow: ${{ github.event.workflow_run.workflow_id }} - workflow_conclusion: success + run-id: ${{ github.event.workflow_run.id }} name: documentation path: documentation-temp + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Store PR id as variable id: pr run: | + pr=$(> $GITHUB_OUTPUT - name: Sync documentation shell: bash + # make sure we override the script coming from the artifact with the version from the main repository run: | + rm ./documentation-temp/docs/sync-web-site.sh + cp -a ./quarkus-main/docs/sync-web-site.sh ./documentation-temp/docs/ chmod 755 ./documentation-temp/docs/sync-web-site.sh ./documentation-temp/docs/sync-web-site.sh main ../../ rm -rf documentation-temp + rm -rf quarkus-main - name: Set up ruby uses: ruby/setup-ruby@v1 with: @@ -94,11 +115,16 @@ jobs: - name: Publishing to surge for preview id: deploy - run: npx surge ./_site --domain https://quarkus-pr-main-${{ steps.pr.outputs.id }}-preview.surge.sh --token ${{ secrets.SURGE_TOKEN }} + run: npx surge@0.23.1 ./_site --domain https://quarkus-pr-main-${PR_ID}-preview.surge.sh --token ${{ secrets.SURGE_TOKEN }} + env: + PR_ID: ${{ steps.pr.outputs.id }} + - name: Update PR status comment on success - uses: actions-cool/maintain-one-comment@v3.2.0 + uses: quarkusio/action-helpers@main with: - token: ${{ secrets.GITHUB_TOKEN }} + action: maintain-one-comment + github-token: ${{ secrets.GITHUB_TOKEN }} + pr-number: ${{ steps.pr.outputs.id }} body: | 🎊 PR Preview ${{ github.sha }} has been successfully built and deployed to https://quarkus-pr-main-${{ steps.pr.outputs.id }}-preview.surge.sh/version/main/guides/ @@ -106,17 +132,15 @@ jobs: - Newsletters older than 3 months are not available. - - body-include: '' - number: ${{ steps.pr.outputs.id }} + body-marker: - name: Update PR status comment on failure + uses: quarkusio/action-helpers@main if: ${{ failure() }} - uses: actions-cool/maintain-one-comment@v3.2.0 with: - token: ${{ secrets.GITHUB_TOKEN }} + action: maintain-one-comment + github-token: ${{ secrets.GITHUB_TOKEN }} + pr-number: ${{ steps.pr.outputs.id }} body: | 😭 Deploy PR Preview failed. - - body-include: '' - number: ${{ steps.pr.outputs.id }} + body-marker: diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 617ca03f970614..68d328b6fbc967 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -16,21 +16,29 @@ jobs: with: distribution: temurin java-version: 17 - - name: Get Date - id: get-date + - name: Generate cache key + id: cache-key run: | - echo "date=$(/bin/date -u "+%Y-%m")" >> $GITHUB_OUTPUT - shell: bash + CURRENT_BRANCH="${{ github.repository != 'quarkusio/quarkus' && 'fork' || github.base_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="m2-cache" + echo "m2-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "m2-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "m2-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + - name: Restore Maven Repository + uses: actions/cache/restore@v4 + with: + path: ~/.m2/repository + key: ${{ steps.cache-key.outputs.m2-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.m2-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.m2-monthly-cache-key }}- - name: Cache SonarCloud packages uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar-${{ steps.get-date.outputs.date }} - - name: Cache Maven packages - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ steps.get-date.outputs.date }} - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 14d6835f2e102f..73645263b000ba 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,21 +2,21 @@ com.gradle develocity-maven-extension - 1.21.4 + 1.22.1 com.gradle common-custom-user-data-maven-extension - 2 + 2.0.1 com.gradle quarkus-build-caching-extension - 1.1 + 1.8 io.quarkus.develocity quarkus-project-develocity-extension - 1.1.0 + 1.1.5 diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar index cb28b0e37c7d20..7967f30dd1d25f 100644 Binary files a/.mvn/wrapper/maven-wrapper.jar and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 7d80d710eaf073..4f926adf77d097 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip -distributionSha256Sum=80b3b63df0e40ca8cde902bb1a40e4488ede24b3f282bd7bd6fba8eb5a7e055c -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar +wrapperVersion=3.3.2 +distributionType=bin +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionSha256Sum=4ec3f26fb1a692473aea0235c300bd20f0f9fe741947c82c1234cefd76ac3a3c +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar diff --git a/.sdkmanrc b/.sdkmanrc index d5b65f8a6ca4bd..8a359b97dd66d1 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.10-tem -mvnd=1.0-m7-m39 +java=17.0.12-tem +mvnd=1.0.2 diff --git a/ACTIONS.md b/ACTIONS.md index 0df1caa935ea36..2518f4c35deb0b 100644 --- a/ACTIONS.md +++ b/ACTIONS.md @@ -1,23 +1,22 @@ # GitHub Actions setup -Quarkus is built using GitHub Actions, with a mix of hosted and self-hosted runners. - -## Setting up a new self-hosted runner on Mac M1 +Quarkus is built using GitHub Actions, with a mix of hosted and self-hosted runners. +## Setting up a new self-hosted runner on Mac M1 ### Non-root user -GitHub Actions should not run with administrator privileges, so will need to be run in a dedicated account. +GitHub Actions should not run with administrator privileges, so will need to be run in a dedicated account. Make an account for the actions runner. The steps below assume the account has the name `githubactions`. -It's usually easiest to do account creation in the GUI, via a VNC connection. Users are managed in +It's usually easiest to do account creation in the GUI, via a VNC connection. Users are managed in the **Users and Groups** section of the Settings app. *Grant administrator privileges to the account* (we will remove them later). - ### System utilities -As administrator, install homebrew. +As administrator, install homebrew. + ```shell /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` @@ -30,9 +29,9 @@ brew install gradle brew install podman ``` -### Podman setup +### Podman setup -Podman needs some [extra configuration](https://quarkus.io/guides/podman) to work with test containers and dev services. +Podman needs some [extra configuration](https://quarkus.io/guides/podman) to work with test containers and dev services. Now log in to the account you created to run the actions. As the `githubactions` user (but with administrator privileges) @@ -41,7 +40,8 @@ As the `githubactions` user (but with administrator privileges) PODMAN_VERSION=`podman -v | sed 's/[a-zA-Z ]*//'` sudo /opt/homebrew/Cellar/podman/$PODMAN_VERSION/bin/podman-mac-helper install ``` -The podman helper install seems to be per-user, but it needs to be done with administrator privileges. + +The podman helper install seems to be per-user, but it needs to be done with administrator privileges. Again as the `githubactions` user, edit ~/.testcontainers.properties and add the following line: `ryuk.container.privileged=true` @@ -63,8 +63,8 @@ rpm-ostree install qemu-user-static systemctl reboot ``` -Now remove administrator privileges from the `githubactions` user. -As with the user creation, it's usually easiest to do this in the GUI. +Now remove administrator privileges from the `githubactions` user. +As with the user creation, it's usually easiest to do this in the GUI. ### Rosetta @@ -73,22 +73,22 @@ A fresh install of macOS will not have Rosetta installed, so Intel binaries cann ```bash softwareupdate --install-rosetta --agree-to-license ``` + ### Stand up the actions runner #### Download the runner scripts -GitHub provides an installation package of customized runner scripts for each repository. +GitHub provides an installation package of customized runner scripts for each repository. Follow [the instructions](https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners) -to install the scripts for the repository to be built. +to install the scripts for the repository to be built. Choose the default group, a descriptive name, and the default work folder. Add a label `macos-arm64-latest`. If you forget the label, it will need to be added [through the UI](https://docs.github.com/en/actions/hosting-your-own-runners/using-labels-with-self-hosted-runners). - #### Cleanup and setup logic -Self-hosted runners do not run on ephemeral hardware, and so workflow runs may need to clean up. -We also need to start a podman machine before the job if one is not already running. +Self-hosted runners do not run on ephemeral hardware, and so workflow runs may need to clean up. +We also need to start a podman machine before the job if one is not already running. To create cleanup and setup scripts and hooks, run: @@ -100,24 +100,22 @@ echo ACTIONS_RUNNER_HOOK_JOB_COMPLETED=/Users/githubactions/runner-cleanup.sh >> echo ACTIONS_RUNNER_HOOK_JOB_STARTED=/Users/githubactions/podman-start.sh >> .env ``` - In the same script, we also ensure that the podman machine is running before jobs execute. -#### Start the runner +#### Start the runner -To test the runner, run +To test the runner, run `run.sh` -Once you're happy the runner is processing builds correctly, it's time to create it as a daemon. +Once you're happy the runner is processing builds correctly, it's time to create it as a daemon. ### Start the service on reboot -Note that GitHub have scripts for this, but theirs run as LaunchAgents, not LaunchDaemons. +Note that GitHub have scripts for this, but theirs run as LaunchAgents, not LaunchDaemons. LaunchAgents run when a user logs in, and LaunchDaemons run on boot, so the daemon option seems preferable. - -As administrator, `sudo vi /Library/LaunchDaemons/actions.runner.quarkusio-quarkus.macstadium-m1.plist`. +As administrator, `sudo vi /Library/LaunchDaemons/actions.runner.quarkusio-quarkus.macstadium-m1.plist`. Then add the following: @@ -164,10 +162,10 @@ Then add the following: ``` -Finally, run +Finally, run ```shell sudo launchctl load -w /Library/LaunchDaemons/actions.runner.quarkusio-quarkus.macstadium-m1.plist ``` -You can check the logs in `/tmp/github.runner.plist.stdout` to confirm everything is working. \ No newline at end of file +You can check the logs in `/tmp/github.runner.plist.stdout` to confirm everything is working. diff --git a/ADOPTERS.md b/ADOPTERS.md index 1a00ce2bf40530..b1976fa44140ec 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -4,31 +4,31 @@ ADOPTERS PUBLIC REFERENCES ----------------- -This document lists the organizations that use Quarkus based on public information available in blog posts and videos. -If any organization would like get added or removed please make a pull request by following these guidelines: +This document lists the organizations that use Quarkus based on public information available in blog posts and videos. +If any organization would like to get added or removed please make a pull request by following these guidelines: * Please don't include your organization's logo or other trademarked material * Add a reference (link to a public blog post, video, slides, etc) mentioning that Quarkus is used -| Organization | Reference | -|-----------------------|-----------------------------------------------------------------------------------| -|Abraxas Informatik AG | https://quarkus.io/blog/abraxas-customer-story/ | -|Artes | https://horvie.github.io/adopt-quarkus/ | -|B<>com | https://5g.labs.b-com.com/ | -|Carrefour | https://horizons.carrefour.com/efficient-java-in-the-cloud-with-quarkus | -|Cytech | https://quarkus.io/blog/cytech-customer-story/ | -|DataCater | https://quarkus.io/blog/datacater-uses-quarkus-to-make-data-streaming-accessible/ | -|Decathlon | https://quarkus.io/blog/decathlon-user-story/ | -|Ennovative Solutions | https://quarkus.io/blog/ennovativesolutions-uses-quarkus-with-aws-lambda/ | -|GoWithFlow | https://quarkus.io/blog/gowithflow-chooses-quarkus-to-deliver-fast-to-production/ | -|Lufthansa Technik | https://quarkus.io/blog/aviatar-experiences-significant-savings/ | -|Logicdrop | https://quarkus.io/blog/logicdrop-customer-story/ | -|[Microcks](https://landscape.cncf.io/?selected=microcks) | https://itnext.io/mocking-and-contract-testing-in-your-inner-loop-with-microcks-part-3-quarkus-devservice-ftw-a14b807737be | -|Payair | https://quarkus.io/blog/why-did-payair-technologies-switch-to-quarkus/ | -|Sedona | https://quarkus.io/blog/sedona-rewrites-insurance-premium/ | -|Stargate | https://quarkus.io/blog/stargate-selects-quarkus-for-its-v2-implementation/ | -|Suomen Asiakastieto Oy | https://quarkus.io/blog/asiakastieto-chooses-quarkus-for-microservices/ | -|Talkdesk | https://quarkus.io/blog/talkdesk-chooses-quarkus-for-fast-innovation/ | -|UTN Faculty Córdoba | https://www.frc.utn.edu.ar/computos/tech/ | -|Vodafone Greece | https://quarkus.io/blog/vodafone-greece-replaces-spring-boot/ | -|Wipro | https://quarkus.io/blog/wipro-customer-story/ | +| Organization | Reference | +|-----------------------|-------------------------------------------------------------------------------------| +|Abraxas Informatik AG | | +|Artes | | +|B<>com | | +|Carrefour | | +|Cytech | | +|DataCater | | +|Decathlon | | +|Ennovative Solutions | | +|GoWithFlow | | +|Lufthansa Technik | | +|Logicdrop | | +|[Microcks](https://landscape.cncf.io/?selected=microcks) | | +|Payair | | +|Sedona | | +|Stargate | | +|Suomen Asiakastieto Oy | | +|Talkdesk | | +|UTN Faculty Córdoba | | +|Vodafone Greece | | +|Wipro | | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd028337de583c..74183fd8497d3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,8 @@ 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) @@ -20,8 +22,8 @@ fixes, documentation, examples... But first, read this page (including the small + [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) + - [`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) @@ -32,11 +34,14 @@ fixes, documentation, examples... But first, read this page (including the small + [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) + * [Don't build any test modules](#dont-build-any-test-modules) + [Automatic incremental build](#automatic-incremental-build) - - [Special case `bom-descriptor-json`](#special-case--bom-descriptor-json-) + - [Special case `bom-descriptor-json`](#special-case-bom-descriptor-json) - [Usage by CI](#usage-by-ci) - [Develocity build cache](#develocity-build-cache) + * [Getting set up](#getting-set-up) + * [-Dquickly](#-dquickly) + * [Benchmarking the build](#benchmarking-the-build) - [Release your own version](#release-your-own-version) - [Documentation](#documentation) * [Building the documentation](#building-the-documentation) @@ -44,7 +49,7 @@ fixes, documentation, examples... But first, read this page (including the small - [Usage](#usage) + [With Maven](#with-maven) + [With Gradle](#with-gradle) - * [MicroProfile TCK's](#microprofile-tck-s) + * [MicroProfile TCK's](#microprofile-tcks) * [Test Coverage](#test-coverage) - [Extensions](#extensions) * [Descriptions](#descriptions) @@ -53,7 +58,8 @@ fixes, documentation, examples... But first, read this page (including the small - [The small print](#the-small-print) - [Frequently Asked Questions](#frequently-asked-questions) -Table of contents generated with markdown-toc + +Table of contents generated with markdown-toc ## Legal @@ -76,7 +82,7 @@ what you would expect to see. Don't forget to indicate your Quarkus, Java, Maven Sometimes a bug has been fixed in the `main` branch of Quarkus and you want to confirm it is fixed for your own application. There are two simple options for testing the `main` branch: -* either use the snapshots we publish daily on https://s01.oss.sonatype.org/content/repositories/snapshots +* either use the snapshots we publish daily on * or build Quarkus locally The following is a quick summary aimed at allowing you to quickly test `main`. If you are interested in learning more details, refer to @@ -86,7 +92,7 @@ the [Build section](#build) and the [Usage section](#usage). Snapshots are published daily with version `999-SNAPSHOT`, so you will have to wait for a snapshot containing the commits you are interested in. -Then just add https://s01.oss.sonatype.org/content/repositories/snapshots as a Maven repository **and** a plugin +Then just add as a Maven repository **and** a plugin repository in your settings xml: ```xml @@ -129,7 +135,7 @@ repository in your settings xml: ``` -You can check the last publication date here: https://s01.oss.sonatype.org/content/repositories/snapshots/io/quarkus/ . +You can check the last publication date here: . ### Building main @@ -189,6 +195,7 @@ is followed for every pull request. can be used temporarily during the review process but things should be squashed at the end to have meaningful commits. We use merge commits so the GitHub Merge button cannot do that for us. If you don't know how to do that, just ask in your pull request, we will be happy to help! +* Please limit the use of lambdas and streams as much as possible in code that executes at runtime, in order to minimize runtime footprint. ### Continuous Integration @@ -227,6 +234,9 @@ If you have not done so on this machine, you need to: * macOS: Use the `Disk Utility.app` to check. It also allows you to create a case-sensitive volume to store your code projects. See this [blog entry](https://karnsonline.com/case-sensitive-apfs/) for more. * Windows: [Enable case sensitive file names per directory](https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity) * Install Git and configure your GitHub access + * Windows: + * enable longpaths: `git config --global core.longpaths true` + * avoid CRLF breaks: `git config --global core.autocrlf false` * Install Java SDK 17+ (OpenJDK recommended) * Install [GraalVM](https://quarkus.io/guides/building-native-image) * Install platform C developer tools: @@ -395,7 +405,7 @@ productive. The following Maven tips can vastly speed up development when workin Let's say you want to make changes to the `Jackson` extension. This extension contains the `deployment`, `runtime` and `spi` modules which can all be built by executing following command: -``` +```shell ./mvnw install -f extensions/jackson/ ``` @@ -407,13 +417,13 @@ all modules in that path recursively. Let's say you want to make changes to the `deployment` module of the Jackson extension. There are two ways to accomplish this task as shown by the following commands: -``` +```shell ./mvnw install -f extensions/jackson/deployment ``` or -``` +```shell ./mvnw install --projects 'io.quarkus:quarkus-jackson-deployment' ``` @@ -427,7 +437,7 @@ Quarkus maintains compatibility as much as possible and for most renamed or move allowing the build to be redirected to the new artifact. However, relocations are not built by default, and to build and install them, you need to enable the `relocations` Maven profile as follows: -``` +```shell ./mvnw -Dquickly -Prelocations ``` @@ -440,7 +450,7 @@ of the `resteasy-jackson` Quarkus integration test (which can be found [here](https://github.com/quarkusio/quarkus/blob/main/integration-tests/resteasy-jackson)). One way to accomplish this is by executing the following command: -``` +```shell ./mvnw test -f integration-tests/resteasy-jackson/ -Dtest=GreetingResourceTest ``` @@ -452,7 +462,7 @@ directory which houses the test project. For example, in order to only run the MySQL test project of the container-image tests, the Maven command would be: -``` +```shell ./mvnw verify -f integration-tests/container-image/maven-invoker-way -Dinvoker.test=container-build-jib-with-mysql ``` @@ -465,7 +475,7 @@ specific integration test mentioned above, `-Dstart-containers` and `-Dtest-cont The following standard Maven option can be used to build with multiple threads to speed things up (here 0.5 threads per CPU core): -``` +```shell ./mvnw install -T0.5C ``` @@ -475,7 +485,7 @@ Please note that running tests in parallel is not supported at the moment! To omit building currently way over 100 pure test modules, run: -``` +```shell ./mvnw install -Dno-test-modules ``` @@ -491,7 +501,7 @@ Instead of _manually_ specifying the modules to build as in the previous example tell [gitflow-incremental-builder (GIB)](https://github.com/gitflow-incremental-builder/gitflow-incremental-builder) to only build the modules that have been changed or depend on modules that have been changed (downstream). E.g.: -``` +```shell ./mvnw install -Dincremental ``` @@ -502,7 +512,7 @@ If you just want to build the changes since the last commit on the current branc comparison via `-Dgib.disableBranchComparison` (or short: `-Dgib.dbc`). There are many more configuration options in GIB you can use to customize its -behaviour: https://github.com/gitflow-incremental-builder/gitflow-incremental-builder#configuration +behaviour: Parallel builds (`-T...`) should work without problems but parallel test execution is not yet supported (in general, not a GIB limitation). @@ -534,16 +544,16 @@ For more details see the `Get GIB arguments` step in `.github/workflows/ci-actio ###### Getting set up -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. +Quarkus has a Develocity instance set up at that can be used to analyze the build performance of the Quarkus project and also provides build cache services. -If you have an account on https://ge.quarkus.io, this can speed up your local builds significantly. +If you have an account on , this can speed up your local builds significantly. 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: -``` +```shell ./mvnw develocity:provision-access-key ``` @@ -553,7 +563,7 @@ From then your build scans will be sent to the Develocity instance and you will You can alternatively also generate an API key from the Develocity UI and then use an environment variable like this: -``` +```shell export DEVELOCITY_ACCESS_KEY=ge.quarkus.io=a_secret_key ``` @@ -640,7 +650,7 @@ When contributing a significant documentation change, it is highly recommended t First build the whole Quarkus repository with the documentation build enabled: -``` +```shell ./mvnw -DquicklyDocs ``` @@ -649,7 +659,7 @@ directory and will avoid a lot of warnings when building the documentation modul Then you can build the `docs` module specifically: -``` +```shell ./mvnw -f docs clean install ``` @@ -682,7 +692,7 @@ To include them into your project a few things have to be changed. *pom.xml* -``` +```xml 999-SNAPSHOT quarkus-bom @@ -698,7 +708,7 @@ To include them into your project a few things have to be changed. *gradle.properties* -``` +```ini quarkusPlatformArtifactId=quarkus-bom quarkusPluginVersion=999-SNAPSHOT quarkusPlatformVersion=999-SNAPSHOT @@ -707,7 +717,7 @@ quarkusPlatformGroupId=io.quarkus *settings.gradle* -``` +```gradle pluginManagement { repositories { mavenLocal() // add mavenLocal() to first position @@ -722,7 +732,7 @@ pluginManagement { *build.gradle* -``` +```gradle repositories { mavenLocal() // add mavenLocal() to first position mavenCentral() @@ -762,7 +772,7 @@ with `-f ...`). ### Descriptions Extensions descriptions (in the `runtime/pom.xml` description or in the YAML `quarkus-extension.yaml`) -are used to describe the extension and are visible in https://code.quarkus.io and https://extensions.quarkus.io. Try and pay attention to it. Here are a +are used to describe the extension and are visible in and . Try and pay attention to it. Here are a few recommendation guidelines: - keep it relatively short so that no hover is required to read it @@ -820,12 +830,14 @@ This project is an open source project, please act responsibly, be nice, polite In the Maven pane, uncheck the `include-jdk-misc` and `compile-java8-release-flag` profiles * Build hangs with DevMojoIT running infinitely - ``` + + ```shell ./mvnw clean install # Wait... [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.192 s - in io.quarkus.maven.it.GenerateConfigIT [INFO] Running io.quarkus.maven.it.DevMojoIT ``` + DevMojoIT require a few minutes to run but anything more than that is not expected. Make sure that nothing is running on 8080. @@ -849,10 +861,12 @@ This project is an open source project, please act responsibly, be nice, polite Just scroll up, there should be an error or warning somewhere. Failing enforcer rules are known to cause such effects and in this case there'll be something like: + ``` [WARNING] Rule 0: ... failed with message: ... ``` + * Tests fail with `Caused by: io.quarkus.runtime.QuarkusBindException: Port(s) already bound: 8080: Address already in use` Check that you do not have other Quarkus dev environments, apps or other web servers running on this default 8080 port. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 23f04b576b7d93..47068dcc390aeb 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -8,7 +8,7 @@ you are more than welcome on our [mailing list](https://groups.google.com/d/foru To help us troubleshoot your issues, we will need some performance insights from your application. -On Linux or macOS, one of the best way to gather performance insights would be to generate CPU and allocation [FlameGraphs](https://github.com/brendangregg/FlameGraph) +On Linux or macOS, one of the best way to gather performance insights would be to generate CPU and allocation [FlameGraphs](https://github.com/brendangregg/FlameGraph) via [Async Profiler](https://github.com/jvm-profiling-tools/async-profiler). If you want a deeper introduction to Async Profiler, do checkout [this article](https://hackernoon.com/profiling-java-applications-with-async-profiler-049s2790). @@ -17,7 +17,7 @@ If you want a deeper introduction to Async Profiler, do checkout [this article]( To install Async Profiler, go to the [release page](https://github.com/jvm-profiling-tools/async-profiler/releases) and download the latest release. -Async Profiler depends on `perf_events`. +Async Profiler depends on `perf_events`. To allow capturing kernel call stacks using `perf_events` from a non-root process, you must first apply a couple OS configuration options. @@ -40,15 +40,16 @@ For allocation profiling, you also need to install HotSpot debug symbol (unless Depending on your Linux and Java distribution this can be done via: ```shell script -# Ubuntu/Debian - Java 8 -apt install openjdk-8-dbg +# Ubuntu/Debian - Java 17 +apt install openjdk-17-dbg -# Ubuntu/Debian - Java 11 - apt install openjdk-11-dbg +# Ubuntu/Debian - Java 21 + apt install openjdk-21-dbg -# On CentOS, RHEL and some other RPM-based distributions - Java 11 -debuginfo-install java-11-openjdk +# On CentOS, RHEL and some other RPM-based distributions - Java 17 +debuginfo-install java-17-openjdk ``` + You can also use a __fastdebug__ build of OpenJdk, this kind of build is not for production use (JVM as assertions are enabled), but it includes debug symbols If needed, see [this](https://github.com/jvm-profiling-tools/async-profiler#allocation-profiling) section in the Async Profiler site for details. @@ -57,13 +58,13 @@ If needed, see [this](https://github.com/jvm-profiling-tools/async-profiler#allo Async Profiler comes with a Java agent, and a command line. -To profile application while it is running, it is recommended to use the command line as you can choose when to start the profiler and prevent your profile data from being bloated with startup events. -This can be important as any application performs a lot of bootstrapping operation upon startup that won't occur at any other during the application lifecycle. +To profile application while it is running, it is recommended to use the command line as you can choose when to start the profiler and prevent your profile data from being bloated with startup events. +This can be important as any application performs a lot of bootstrapping operation upon startup that won't occur at any other during the application lifecycle. By starting the profiling on demand, you prevent these bootstrap instructions from being part of the profile data. When you use the command line, it is advised to use `-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints` JVM flags to have more accurate results. -It is usually advised to profile an application under load, +It is usually advised to profile an application under load, and to start profiling only after some warmup time to allow Java's Just In Time compiler to optimize your application code (not to mention giving the opportunity for database caches to warmup, etc...). Such load could be created by a load generator tool ([ab](https://httpd.apache.org/docs/2.4/programs/ab.html), [wrk2](https://github.com/giltene/wrk2), [Gatling](https://gatling.io/), [Apache JMeter](https://jmeter.apache.org/), ...). @@ -77,7 +78,7 @@ To start CPU profiling, execute the following command: `-b 4000000` is used to increase the frame buffer size as the default is often too small. -To end profiling and gather the results you can launch the same command with the `stop` subcommand, this will tell you if the buffer frame was too small. +To end profiling and gather the results you can launch the same command with the `stop` subcommand, this will tell you if the buffer frame was too small. The output is a text file that is not really usable, so let's use our preferred performance representation: the flame graph. ```shell script @@ -87,8 +88,8 @@ The output is a text file that is not really usable, so let's use our preferred It will create an HTML flame graph (Async Profiler automatically detect that you ask for a flame graph thanks to the `html` file extension) that you can open in your browser (and even zoom inside it by clicking on a frame). -One very useful option is `-s` (or `--simple`) that results in simple class names being used instead of fully qualified class names, -thus making the flame graph more readable (at cost of not showing the package names of classes). +One very useful option is `-s` (or `--simple`) that results in simple class names being used instead of fully qualified class names, +thus making the flame graph more readable (at cost of not showing the package names of classes). You can also limit the profiling duration by using `-d` (or `--duration`) followed by the duration in seconds. If you use the `--duration` option, the output file will be created automatically at the end of the duration period. You do not need to explicitly start and stop the profiler. @@ -129,13 +130,13 @@ java -agentpath:/path/to/async-profiler/build/libasyncProfiler.so=start,event=al Note that short options are not supported inside the agent, you need to use their long versions. -By default, Async Profiler sample events every 10ms. -When it comes to profiling / debugging a Quarkus startup issue, this value is often too high as Quarkus starts very fast. +By default, Async Profiler sample events every 10ms. +When it comes to profiling / debugging a Quarkus startup issue, this value is often too high as Quarkus starts very fast. For that reason, it is not uncommon to configure the profiling interval to 1000000ns (i.e. 1ms). ## Profiling application dev mode with Async Profiler -For profiling Quarkus dev mode, the Java agent is again necessary. +For profiling Quarkus dev mode, the Java agent is again necessary. It can be used in the same way as for the production application with the exception that `agentpath` option needs to be set via the `jvm.args` system property. ```shell script @@ -150,13 +151,13 @@ You can also configure the `jvm.args` system property directly inside the `quark ## Analysing build steps execution time -When trying to debug startup performance, it is convenient to log build steps execution time. +When trying to debug startup performance, it is convenient to log build steps execution time. This can be achieved by adding the following system property: `-Dquarkus.debug.print-startup-times=true` in dev mode or when launching the JAR. -There is also a nice visualization of build steps available in the Dev UI located here: http://localhost:8080/q/dev/build-steps. +There is also a nice visualization of build steps available in the Dev UI located here: . -If you want to have the same visualization of build steps processing when building your application, you can use the `quarkus.debug.dump-build-metrics=true` property. -For example using `mvn package -Dquarkus.debug.dump-build-metrics=true`, will generate a `build-metrics.json` in your `target` repository that you can process via the quarkus-build-report application available here https://github.com/mkouba/quarkus-build-report. +If you want to have the same visualization of build steps processing when building your application, you can use the `quarkus.debug.dump-build-metrics=true` property. +For example using `mvn package -Dquarkus.debug.dump-build-metrics=true`, will generate a `build-metrics.json` in your `target` repository that you can process via the quarkus-build-report application available here . This application will generate a `report.html` that you can open in your browser. ## What about Windows? @@ -174,4 +175,4 @@ Here we configure JFR with a deeper stack depth as the default is usually not en ## What about native executables? If you are having performance issues with native builds of your application first make sure that these issues only manifest in native mode. -If so, please consult the [native reference guide](https://quarkus.io/guides/native-reference) and more specifically the [profiling section](https://quarkus.io/guides/native-reference#profiling). \ No newline at end of file +If so, please consult the [native reference guide](https://quarkus.io/guides/native-reference) and more specifically the [profiling section](https://quarkus.io/guides/native-reference#profiling). diff --git a/adr/0001-community-discussions.adoc b/adr/0001-community-discussions.adoc index 5708ac9269b74c..ca0970d5a66159 100644 --- a/adr/0001-community-discussions.adoc +++ b/adr/0001-community-discussions.adoc @@ -9,20 +9,20 @@ Quarkus community is growing and until now we've catered very well for core cont We have multiple communication channels: https://github.com/quarkusio/quarkus[issues], https://groups.google.com/g/quarkus-dev?pli=1[quarkus-dev mailing list], https://quarkusio.zulipchat.com[zulip], https://stackoverflow.com/questions/tagged/quarkus[stackoverflow] -Isues are great for bugs/feature work. Mailing list for design conversations between developers, chat for watercooler style discussions and stackoverflow for user questions. +Issues are great for bugs/feature work. Mailing list for design conversations between developers, chat for watercooler style discussions and stackoverflow for user questions. This setup has issues though, some are: -- zulip chat is used for a lot of users questions but none of that is easily searchable/discoverable so it is very synchronous, -- people reported that they don't feel okey posting on quarkus-dev or zulips as it is seems focused on dev work and not so much about community events, jobs, conferences, etc. +- Zulip chat is used for a lot of users questions but none of that is easily searchable/discoverable so it is very synchronous, +- people reported that they don't feel okey posting on quarkus-dev or Zulip as it seems focused on dev work and not so much about community events, jobs, conferences, etc. - its hard to monitor as contributor who wants to help answer/ask questions. - Users reported they do not have access to Zulip chat due to corporate or company policies/proxies. They do have access to GitHub. -How can we improve this situaton and enable the broader community to more easily ask questions and find answers - without it all be relying on just a few Quarkus core contributors? +How can we improve this situation and enable the broader community to more easily ask questions and find answers - without it all be relying on just a few Quarkus core contributors? == Scenarios (optional) -User wants to locate a answer to a question - zulip chats does not show up in google search; stackoverflow might but might not have an answer. +User wants to locate a answer to a question - Zulip chats does not show up in google search; stackoverflow might but might not have an answer. Contributor want to arrange or attend an event around Quarkus - where do he post/look for info on that ? @@ -54,7 +54,7 @@ Enable GitHub discussions feature on `quarkusio/quarkusio` with the following in - Announcements (post only by admins) - Introductions (posts by anyone, optional place introduce yourself) -- Comunity (post by anyone, general discussions) +- Community (post by anyone, general discussions) - Q&A (post by anyone, Q&A mode enabled) - Quarkus Insights Episodes (show notes and offline comments) - Events (Setup and announce of interest or other events) diff --git a/adr/0003-websocket-next-client.adoc b/adr/0003-websocket-next-client.adoc new file mode 100644 index 00000000000000..642310e655b172 --- /dev/null +++ b/adr/0003-websocket-next-client.adoc @@ -0,0 +1,195 @@ += WebSocket Client API + +* Status: proposed +* Date: 2024-06-04 by @cescoffier, @geoand, @mkouba +// * Revised: + +== Context and Problem Statement + +Currently, Quarkus relies on the https://jakarta.ee/specifications/websocket/[Jakarta WebSocket API] to provide WebSocket client support. This API has several limitations: + +- It is blocking and does not adhere to the reactive programming model. While the API is async, it relies on thread pools and do not use an event loop model. It makes building Quarkus application with such a client not as efficient as it could be. +- The API is not very user-friendly. It is verbose and does not provide a good developer experience. For example, the received message are not mapped to a Java object, and the developer has to manually parse the message. +- The integration with CDI is not optimal. The API does not provide a good way to inject the WebSocket client in a CDI bean, and the client does not define a lifecycle that can be managed by CDI. + +Thus, along the WebSocket Next server effort, we propose to introduce a new WebSocket client API that addresses these limitations. + +The main objectives of this proposal are: + +- Provide a WebSocket client API that adhere to the reactive model of Quarkus while also providing a good blocking developer experience. +- Provide a way to map the received messages to Java objects. +- The definition of a clear CDI-based lifecycle for the WebSocket client. +- The possibility to inject the WebSocket client in a CDI bean +- The possibility to instrument the client to handle security, metrics, observability, etc. + +It does not replace the existing WebSocket client API but provides an alternative that is more suitable for Quarkus applications. + +== Proposed Solution + +We propose to introduce a new WebSocket client API based on the Vert.x WebSocket client. Thus is will adhere to the event loop model used by Quarkus. For the end-user, the API is very similar to the WebSocket Next server API. + +=== WebSocket Client Endpoints + +The main concept is the introduction of WebSocket client endpoints. A client endpoint is a class annotated with `@WebSocketClient(path)`. The path is the URL of the WebSocket server, which can contain path parameters. The class can define methods annotated with `@OnOpen`, `@OnTextMessage`, `@OnBinaryMessage, `@OnClose`, and `@OnError`. The methods are called when the corresponding event occurs. The class is automatically considered as a _singleton_ CDI bean. + +A client endpoint can also inject the `WebSocketClientConnection` object which is related to the current connection. The `WebSocketClientConnection` object provides methods to send messages, close the connection, etc. The `WebSocketClientConnection` object is also a CDI bean and can be injected in other beans. It is a _session-scoped_ +CDI bean, where the session starts when the connection is established and ends when the connection is closed. + +Here is an example of a WebSocket client endpoint: + +[source, java] +---- +@WebSocketClient(path = "/ws/{name}") +public class ClientEndpoint { + @OnTextMessage + Echo echo(EchoMessage message, WebSocketClientConnection connection, @PathParam String name) { + return Echo.from(message); + } +} +---- + +The `message` is automatically mapped to Java object as well as the `Echo` instance returned by the method. +The deserialization and serialization follows the same rules as the WebSocket Next server API. The `@PathParam` annotation is used to extract the path parameter from the URL. + +The execution model of each method depends on the method signature and the presence of the `@Blocking` annotation. If the method returns a `Uni`, `Multi`, `CompletionStage`, or `Publisher`, the method is considered as reactive and executed on the event loop. If the method returns a `void` or an object, the method is considered as blocking and executed on a worker thread. The `@Blocking` annotation can be used to force the method to be executed on a worker thread. The `@NonBlocking` annotation can be used to force the method to be executed on the event loop. Finally, blocking method can also be executed on a virtual thread by using the `@RunOnVirtualThread` annotation. + +The client endpoint feature allows to encapsulate the WebSocket client logic in a single class. Because it is a CDI bean, it can be easily injected in other beans, or use CDI events to communicate the received WebSocket messages with the rest of the application. + +=== WebSocket Connector + +The second concept introduced by the API is the WebSocket _connector_. The connector is used to configure and create new connections. +While the client endpoint defines the methods to be called when an event occurs (message, connection opened), the connector is used to create the connection and configure it. The connector is a CDI bean and can be injected in other beans: + +Let's consider the following client endpoint: + +[source, java] +---- +@WebSocketClient(path = "/endpoint/{name}") +public static class ClientEndpoint { + + + @OnTextMessage + void onMessage(@PathParam String name, String message, WebSocketClientConnection connection) { + // ... + } + + @OnClose + void close() { + // ... + } + +} +---- + +This endpoint is used as follows: + +[source, java] +---- +// <1> Injection of the connector +@Inject +WebSocketConnector connector; + +// <2> Create the connection and configure the uri and path parameters (if any) +WebSocketClientConnection connection = connector + .baseUri(uri) + .pathParam("name", "Roxanne") + .connectAndAwait(); +// <3> Use the connection to send messages if needed (the client endpoint can also retrieve the connection) +connection.sendTextAndAwait("Hi!"); +---- + +In this example, the connector is injected in a bean. The connector is used to create a new connection. The connection is configured with the base URI and path parameters. The connection is then established by calling the `connectAndAwait` method (asynchronous methods are also available). The connection can be used to send messages, close the connection, etc. + +The duality between the _client endpoint_ and the _connector_ separates the configuration and establishment of the websocket connection from the logic. The _client endpoint_ is used to define the logic of the WebSocket client, while the _connector_ is used to configure and create the connection. + +If an application tries to inject a _connector_ for a missing endpoint, an error is thrown. + +=== Basic Connector + +In the case where the application developer does not need the combination of the _client endpoint_ and the _connector_, a basic connector can be used. The basic connector is a simple way to create a connection and send messages without defining a _client endpoint_: + +[source, java] +---- +@Inject +BasicWebSocketConnector connector; // <1> Inject the basic connector + +// ... + +// <2> Configure the connection and create it + WebSocketClientConnection connection2 = BasicWebSocketConnector + .create() + .baseUri(uri) + .path("/ws") + .executionModel(ExecutionModel.NON_BLOCKING) + +// <3> Register callbacks directly on the connection + .onTextMessage((c, m) -> { + // ... + }) + .connectAndAwait(); +---- + +The basic connector is closed to a low-level API and is reserved for advanced users. +However, unlike others low-level WebSocket clients, it is still a CDI bean and can be injected in other beans. +It also provides a way to configure the execution model of the callbacks, ensuring the optimal integration with the rest of Quarkus. + +=== Client limitations + +While the client endpoint class reuses annotations that can also used on the server side, note that some features are not supported on the client side. +Typically, it is not possible to _broadcast_ a message from a client, as the client is only connected to a single server. + +=== Listing active client connections + +It is possible for an application to list the active connections by injecting the `OpenClientConnections` bean. +This bean provides a method to list the active connections: + +[source, java] +---- +@Inject +OpenClientConnections connections; + +// ... + +connections.listAll(); // List all connections +connections.findByConnectionId("..."); // Find a connection by its id +connections.findByClientId("..."); // Find a connection by its client id +---- + +`OpenClientConnections` allows retrieving connections using the regular connector and the basic connector. + + +== Considered options + +=== Using the existing WebSocket API + +We could improve the existing WebSocket API by providing a better integration with CDI and a better developer experience. However, the API is blocking and does not adhere to the reactive model of Quarkus. It would be difficult to provide a good developer experience without a complete rewrite of the API. + +Also, the current API is specified by the Jakarta EE specification, and we would like to avoid breaking changes in the specification. + +=== Only propose a low level client API + +We could only propose a low-level client API that allows to create WebSocket connections and send messages. However, this would not provide a good developer experience and would not be very useful for Quarkus applications. + +This approach is still possible by instantiating the Vert.x WebClient directly. +However, we would not be able to implement observability, metrics, CDI lifecycle management, etc. + +It is reserved for advanced users. + +=== Using a declarative approach receiving callbacks + +An alternative has been considered where the WebSocket client would be configured using a declarative approach. The user would define a configuration file that specifies the WebSocket client configuration and the callbacks to be called when an event occurs. This approach has been rejected because it is not very user-friendly and does not provide a good developer experience. Passing callbacks is cumbersome and paves the road to complex execution model mismatches. + +== Consequences + +=== Positive + +* A new WebSocket client API that is more suitable for Quarkus applications. +* A better developer experience when building WebSocket clients. +* A better integration with CDI. + +=== Negative + +* Moving away from standard APIs (which means another API to learn). + + + diff --git a/adr/0004-using-the-tls-registry-for-clients.adoc b/adr/0004-using-the-tls-registry-for-clients.adoc new file mode 100644 index 00000000000000..01fcc6fbbba5fa --- /dev/null +++ b/adr/0004-using-the-tls-registry-for-clients.adoc @@ -0,0 +1,189 @@ += Using the TLS registry for clients + +* Status: accepted +* Date: 2024-06-12 by @cescoffier, @geoand, @gsmet +// * Revised: + +== Context and Problem Statement + +With https://github.com/quarkusio/quarkus/pull/39825[#39825], we introduced the concept of a TLS registry in Quarkus. +The TLS registry is a way to configure TLS settings in a central place and reuse them across the application and extensions. +This is particularly useful when an application needs to connect to multiple services using TLS. + +Until now, each extension has _maintained_ its own way to configure TLS settings. This leads to a lot of duplication and inconsistencies. +It's also difficult for the user to understand how to configure TLS settings in Quarkus. +The TLS registry is a way to solve this problem. + +The goal of this ADR is to explain how the registry should be used by client establishing a connection to a server using TLS. + +It should be noted that TLS is a sensitive and complex topic. +The TLS registry is not a silver bullet and does not solve all the problems related to TLS. +It is a way to simplify the configuration of TLS settings in Quarkus applications. + +== The TLS registry + +The TLS registry is an extension processing the `quarkus.tls` configuration and proposing a runtime API to access it. +Note that the configuration and the runtime API are different. +The configuration is used to define the TLS settings, while the runtime API is used to access these pre-processed settings. + +hen using the options directly under `quarkus.tls.`, one configures the default (_unamed_) configuration, while using options under `quarkus.tls..` configures a _named_ configuration. +For each configuration, trust stores and key stores can be defined, as well as the default protocol, cipher suites, etc. +More details can be found in the https://quarkus.io/version/main/guides/tls-registry-reference[documentation] and in the https://github.com/quarkusio/quarkus/blob/main/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/TlsBucketConfig.java[code]. + +At runtime, the https://github.com/quarkusio/quarkus/blob/main/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/TlsConfigurationRegistry.java[TlsConfigurationRegistry] CDI bean gives access to the default and named _TlsConfiguration_. +These configurations have been verified during the application startup (password, alias, validity...) and are ready to be used. +https://github.com/quarkusio/quarkus/blob/main/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/TlsConfiguration.java[TLSConfiguration] gives access to the key store, trust store, Vert.x options, tailored `SSLContext`... +It also supports reloading the certificates. + +== Configuring clients with the TLS registry + +When configuring a _client_ establishing a connection to a server using TLS, the TLS registry should be used to configure the TLS settings. +The TLS settings must be defined in the `quarkus.tls..` configuration. + +WARNING: Using the default configuration is going to create conflict with the server configuration, and it's is generally not a good idea to use the same configuration for the client and the server. + +The client extension should use the TLS registry to retrieve the _TlsConfiguration_ and use it to configure the connection. + +Thus, the client configuration must be extended with a new configuration item: `tlsConfigurationName`: + +[source, java] +---- +/** + * The name of the TLS configuration to use. + *

+ * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * The default TLS configuration is not used by default. + */ +Optional tlsConfigurationName(); +---- + +Clients must not use the default TLS configuration, except for one case. +For backward compatibility purpose, `quarkus.tls.trust-all` should be honored when configuring clients. + +== Using the TLS registry from client extensions + +=== Retrieving the registry from an extension + +The TLS registry is a CDI bean and can be injected in other beans. +The TLS registry is a _singleton_ CDI bean. + +However, before configuring the client, you need to make sure the TLS registry is properly initialized. +The recommended approach is to inject the `TlsConfigurationRegistry tlsConfigurationRegistry` bean in a `recorder`: + +[source, java] +---- +BasicWebSocketConnectorImpl(Vertx vertx, Codecs codecs, ClientConnectionManager connectionManager, + WebSocketsClientRuntimeConfig config, TlsConfigurationRegistry tlsConfigurationRegistry) { + // ... +} +---- + +Another approach is to use the `TlsRegistryBuildItem` in a _processor_: + +[source, java] +---- +@Record(ExecutionTime.RUNTIME_INIT) +@BuildStep +public SyntheticBeanBuildItem setup(OidcBuildTimeConfig oidcBuildTimeConfig, OidcConfig oidcRunTimeConfig, + KeycloakPolicyEnforcerConfig keycloakConfig, KeycloakPolicyEnforcerRecorder recorder, + HttpConfiguration httpConfiguration, TlsRegistryBuildItem tlsRegistryBuildItem) { + if (oidcBuildTimeConfig.enabled) { + return SyntheticBeanBuildItem.configure(PolicyEnforcerResolver.class).unremovable() + .types(PolicyEnforcerResolver.class) + .supplier(recorder.setup(oidcRunTimeConfig, keycloakConfig, httpConfiguration, + tlsRegistryBuildItem.registry())) // Get the registry runtime value + .scope(Singleton.class) + .setRuntimeInit() + .done(); + } + return null; +} +---- + +=== Finding the named TLS configuration + +Once the TLS registry is properly passed, the client extension can use it to retrieve the named TLS configuration: + +[source, java] +---- +TlsConfiguration configuration = null; +Optional maybeTlsConfiguration = TlsConfiguration.from(tlsConfigurationRegistry, + config.tlsConfigurationName()); +if (config.tlsConfigurationName.isPresent()) { + configuration = maybeConfiguration.get(); +} else if (tlsRegistry.getDefault().isPresent() && tlsRegistry.getDefault().get().isTrustAll()) { + defaultTrustAll = tlsRegistry.getDefault().get().isTrustAll(); + if (defaultTrustAll) { + LOGGER.warn("The default TLS configuration is set to trust all certificates. This is a security risk." + + "Please use a named TLS configuration for the mailer " + name + " to avoid this warning."); + } +} +---- + +Also, using the _default_ `trust-all` should be avoided, and a warning should be logged if it's used. + +=== Configuring a Vert.x client + +When configuring a Vert.x client (HTTP client, SMTP client, gRPC client, etc.), the `TlsConfiguration` should be used to configure the client: + +- The `trustStore` and `keyStore` should be set on the `options.setTrustOptions(config.getTrustStoreOptions())` and `options.setKeyCertOptions(config.getKeyStoreOptions())`. Setting the keystore enables mutual TLS (`mTLS`). +- `trustAll` can be set using `options.setTrustAll(true)`. +- The hostname verification algorithm should be configured to a sensible default (`HTTPS` for every HTTP based protocol). +To disable the hostname verification, the `NONE` value should be used. +- `TlsConfigure.getSSLOptions()` allows configuring various TLS aspect like the protocol, cipher suites, handshake, ALPN, certificate revocation lists, etc: + +[source, java] +---- + SSLOptions sslOptions = configuration.getSSLOptions(); +if (sslOptions != null) { + cfg.setSslHandshakeTimeout(sslOptions.getSslHandshakeTimeout()); + cfg.setSslHandshakeTimeoutUnit(sslOptions.getSslHandshakeTimeoutUnit()); + for (String suite : sslOptions.getEnabledCipherSuites()) { + cfg.addEnabledCipherSuite(suite); + } + for (Buffer buffer : sslOptions.getCrlValues()) { + cfg.addCrlValue(buffer); + } + cfg.setEnabledSecureTransportProtocols(sslOptions.getEnabledSecureTransportProtocols()); + +} +---- + +- For protocol supporting SNI, the SNI should be configured using `options.setSNI(tlsConfiguration.usesSNI())`. + + +=== Configuring non-Vert.x clients + +When configuring a non-Vert.x client, the `TLSConfiguration` provides: + +- `KeyStore` objects for the key store and trust store +- A `SSLContext` object to configure the SSL context. + +== Considered options + +=== Continuing with the current approach + +The current approach is to let each extension manage its TLS settings. +This approach leads to a lot of duplication and inconsistencies. +It's also difficult for the user to understand how to configure TLS settings in Quarkus. + +Note that the TLS registry does **NOT** replace the extension-specific configuration. +It's a way to simplify the configuration of TLS settings in Quarkus applications. +The extension-specific configuration should be considered _deprecated_ and users should be invited to use the TLS registry. + +== Consequences + +=== Positive + +* A centralized way to configure TLS settings in Quarkus applications. +* A homogenization of the TLS configuration across Quarkus extensions. +* A more complete and consistent TLS configuration. + +=== Negative + +* Dealing with 2 different ways to configure TLS settings in Quarkus applications until the extension-specific configurations are removed. + + + diff --git a/adr/0005-working-group-process.adoc b/adr/0005-working-group-process.adoc new file mode 100644 index 00000000000000..14e7d1cb577240 --- /dev/null +++ b/adr/0005-working-group-process.adoc @@ -0,0 +1,170 @@ += Working Groups + +* Status: _accepted_ +* Date: 2024-07-15 by @cescoffier + +== Context and Problem Statement + +Quarkus is a large project with many contributors. +It's hard to keep track of all the initiatives and ensure that the community is aware of ongoing work. +We need a way to organize work around specific topics and ensure that the community is aware of these initiatives. + +We also need to ensure that the work is done transparently and that the community can participate in the discussions. + +This would be the basis of an informal roadmap, where the community can see what is being worked on and what is coming next. +Our previous attempts to publish and maintain a roadmap were not successful. +We need a more lightweight approach, focusing on current work and next steps. + +Additionally, new contributors may find it hard to find a way to contribute to the project, as the project's size may be overwhelming. +Working on a specific area or topic may be more appealing to new contributors and may help them get started. + +== Working Groups + +The idea behind this proposal is to introduce the concept of _working groups_. +A working group is a lightweight way to organize work around a specific topic. +It aims to gather people interested in a specific topic and ensure that the work is done transparently. +It also aims to ensure that the community is aware of ongoing work and can participate in the discussions. + +=== Defining a Working Group + +To kick off a working group, let’s make sure we know what we’re getting into. +Here’s a simple checklist to keep things clear and manageable: + +1. Clear Goal: What exactly do the working group want to achieve? +Make sure the group has a straightforward, easy-to-understand goal. +The scope of the group must be carefully defined. +2. Trackable Progress: How will we know the group is making progress? +GitHub issues will be the primary way to publicize the progress. +Other means like regular GitHub project updates can be used. +3. Realistic Aim: The working group goal must be achievable within a reasonable timeframe. +It’s better to break down large ideas into smaller working groups, one at a time. +4. End in Sight: When will we be done? Even if there’s no strict deadline, a working group should have an idea of what ‘done’ looks like. + +Once the scope of a working group is defined, it should be announced on GitHub discussions under the https://github.com/quarkusio/quarkus/discussions/categories/design-discussions[Design Discussion category]. +This way, the community can be aware of ongoing work and participate in the discussions. +During that time, the definition of the working group can be refined based on the feedback received. + +Here are a few examples: + +- https://github.com/quarkusio/quarkus/discussions/41309[Working Group: Static Site Generation] +- https://github.com/quarkusio/quarkus/discussions/38473[Working Group: WebSocket Next] +- https://github.com/quarkusio/quarkus/discussions/41867[Working Group: Test classloading] + +=== Organizing a Working Group + +Once a working group has garnered enough interest, a project board should be created, and a main point of contact should be identified. +A (public) project board should be used to track the progress of the working group. +It gathers all the related issues and PRs and should be updated regularly. + +It is recommended to use a simple template for the project board, with columns like "to do," "in progress," and "done." +The board should be updated regularly. +The _status_ of the working group should be updated, and the related issues should be added to the board. +It is important that the board does not remain stale. + +Depending on where the main part of the work is done, the board can be created in the Quarkus organization or in the Quarkiverse organization. + +On the board, a short description of the working group should be added, along with the proposed scope and the main point of contact. + +=== Point of Contact and Communication + +The point of contact is the main entry point for the working group. +Both the community and the team can reach out to this person to get more information about the working group or to participate. +The point of contact should be available on GitHub and Zulip, ensuring that communication is done transparently. +A working group may have multiple points of contact, depending on the size and scope of the group. + +Most communication should be done on GitHub discussions, issues, and PRs. +If the working group needs to organize calls, these calls should be open to everyone in the community. +It is important for the working group to publish the outcome of these discussions and possible decisions made during these calls. + +=== Participating in a Working Group + +Anyone can participate in a working group. +The working group should be open to everyone, and the discussions should be done transparently. +The point of contact and the other contributors should ensure that the discussions are respectful and that everyone can participate and contribute. + +=== Driving a Working Group + +Ideally, once a week, an update should be posted on the board and on the GitHub discussion. +The update should summarize the progress made during the week, the next steps, and include a status (on track, at risk, off track, complete). +It's important to keep the community aware of ongoing work and ensure that the working group is making progress, identifying the next steps, and so on. + +It might be interesting to publish demos, blog posts, or other content to keep the community aware of ongoing work. + +=== Completing a Working Group + +Once the goal of the working group is achieved, the working group should be closed. +The outcome of the working group should be published on GitHub discussions, and the project board should be archived (status set to `complete`). + +The outcome of a working group can be various: + +- _Technical contribution_: It can be a set of identified issues and PRs that have been resolved. +- _ADR_: The outcome of a working group may end up proposing an ADR to capture the decisions made during the working group. +- _Documentation_: The outcome of a working group may be a set of documentation updates. +- _Blog posts / Demos / Videos_: The outcome of a working group may be a blog post to summarize the work done or a demo/video. +- _Exploratory work_: The outcome of a working group may be a set of exploratory work that will be used to drive the next steps. + +=== Maximum Number of Working Groups + +We should limit the number of working groups running concurrently to avoid overwhelming contributors. +The exact number should be defined based on the capacity of the team and the community. +It is better to have a few working groups that are making progress than many working groups that are stalled. + +=== Working Group Lifecycle + +The lifecycle of a working group is as follows: + +1. Define the scope of the working group +2. Announce the working group on GitHub discussions +3. Organize the working group +4. Drive the working group +5. Complete the working group + +Once a working group is completed, the outcome should be published on GitHub discussions, and the project board should be archived. + +=== Working Group vs. Rest of the Work + +Not all work should be done in working groups. +Working groups are a way to organize work around specific topics, but they should not be the only way to contribute to the project. +Working groups should be used to drive specific initiatives, but the rest of the work should be done as usual. + +== Considered Options + +=== Status Quo + +We continue to work as we are doing now, without any specific organization around the work. +Under this option, we would not have a way to organize work around specific topics, and the community would not be aware of ongoing work. +It makes it harder for new contributors to find a way to contribute to the project and to understand the roadmap of the project. + +This approach has been tried in the past and has not been successful. + +=== More Formal Organization + +We could introduce a more formal organization around the work, with a more detailed roadmap and a more structured way to organize the work. +This would require more resources and more time to maintain, and it may be harder to keep up to date. +It may also be harder for the community to participate in the discussions, making a clear distinction between the _core_ team and the community. + +=== Considered Names + +We have considered various names for the _working group_. +Task force, working group, tiger team, tribe, etc., are some of the names that have been considered. + +We have chosen _working group_ as it is a simple and clear name that reflects the purpose of the group. +One of the considered benefits is its abbreviation, _WG_, which is easy to understand. + +== Consequences + +=== Positive + +* The community is aware of ongoing work and can participate in the discussions. +* New contributors can find a way to contribute to the project. +* The work is done transparently. +* The work is organized around specific topics. +* The community can see what is being worked on and what is coming next. + +=== Negative + +* It requires more work to organize the working groups. +* It requires more work to keep the working groups up to date. +* It may be harder to limit the number of working groups running concurrently. + +The proposed working group process is designed to be lightweight and should not require too much overhead, but any coordination effort requires some work. diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 249a85cdee5ebc..e2d597922f2b38 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -18,52 +18,53 @@ 1.78.1 1.0.2.5 1.0.19 + 9.0.5 5.0.0 3.0.2 - 3.2.0 + 3.2.2 1.3.2 1 - 1.1.6 + 1.1.7 2.1.5.Final - 3.1.2.Final - 6.2.9.Final + 3.1.3.Final + 6.2.10.Final 0.33.0 0.2.4 0.1.15 0.1.5 - 1.32.0 - 1.32.0-alpha - 1.21.0-alpha - 5.2.2.Final - 1.12.5 - 2.1.12 + 1.39.0 + 2.5.0-alpha + 1.26.0-alpha + 5.3.2 + 1.13.4 + 2.2.2 0.22.0 - 21.3 + 22.2 3.1 4.0.1 4.0.1 1.3 3.0 4.0.2 - 3.0 + 3.0.1 3.0.1 2.1 2.0 3.1.1 - 2.3.0 - 3.8.1 + 2.6.0 + 3.9.1 4.1.0 4.0.0 - 3.10.0 - 2.8.4 - 6.3.0 - 4.5.2 - 2.1.0 + 3.12.0 + 2.10.0 + 6.4.0 + 4.6.0 + 2.1.2 1.0.13 3.0.1 - 3.12.0 - 4.21.0 - 2.6.0 + 3.15.0 + 4.24.0 + 2.7.0 2.1.3 3.0.0 3.0.0 @@ -92,144 +93,137 @@ 23.1.2 1.8.0 - 2.17.1 + 2.17.2 1.0.0.Final - 3.14.0 - 1.17.0 + 3.17.0 + 1.17.1 1.7.0 - - 6.5.2.Final - 1.14.15 - 6.0.6.Final - 2.3.0.Final - 8.0.1.Final - - 7.1.1.Final - 7.0.1.Final - 2.4 + + 7.0.2.Final + 2.5 8.0.0.Final - 8.13.4 + 8.15.1 2.2.21 2.2.5.Final 2.2.2.Final - 2.2.1.Final + 3.0.1.Final 2.0.0.Final 1.7.0.Final 1.0.1.Final - 2.4.2.Final - 2.1.4.SP1 + 2.5.2.Final + 2.2.1.Final 3.6.1.Final - 4.5.7 + 4.5.10 4.5.14 4.4.16 4.1.5 9.2.1 2.3.2 - 2.2.224 - 42.7.3 - 3.4.0 + 2.3.230 + 42.7.4 + 3.4.1 8.3.0 - 12.6.1.jre11 + 12.8.1.jre11 1.6.7 - 23.3.0.23.09 - 10.14.2.0 + 23.5.0.24.07 + 10.16.1.1 11.5.8.0 1.2.6 2.2 - 5.10.2 - 15.0.4.Final - 5.0.4.Final + 5.10.3 + 15.0.8.Final + 5.0.8.Final 3.1.5 - 4.1.108.Final + 4.1.111.Final 1.16.0 1.0.4 - 3.6.0.Final - 2.6.0 - 3.7.0 + 3.6.1.Final + 2.6.2 + 4.0.5 + 3.7.1 1.8.0 1.1.10.5 - 0.100.0 + 0.107.0 2.13.14 1.2.3 - 3.11.5 - 2.15.3 + 3.13.0 + 2.18.2 + 3.0.0 3.1.0 1.0.0 - 2.0.0 - 1.8.1 + 2.0.20 + 1.9.0 0.27.0 - 1.6.2 - 4.1.2 + 1.7.2 + 4.1.4 3.2.0 - 4.2.1 + 4.2.2 3.0.6.Final - 10.13.0 - 3.0.3 + 10.18.0 + 3.0.4 - 4.27.0 - 4.24.0 - 2.2 + 4.29.1 + 4.29.1 + 2.3 6.0.0 - 4.11.1 - 1.8.0 + 5.1.4 + 1.11.0 0.34.1 - 3.25.10 + 3.26.2 0.3.0 - 4.13.1 - 5.2.SP7 - 2.1.SP2 - 5.4.Final - 2.1.SP1 - 5.12.0 + 4.17.0 + 6.1.SP3 + 3.2.SP2 + 6.2 + 3.2 + 5.13.0 5.8.0 - 4.13.0 - 2.0.3.Final - 24.0.4 + 2.1.0 + 25.0.6 1.15.1 - 3.43.0 - 2.27.1 + 3.47.0 + 2.32.0 0.27.0 - 1.44.2 + 1.45.0 2.1 4.7.6 1.1.0 - 1.26.1 + 1.27.1 1.12.0 2.11.0 - 1.1.2.Final - 2.23.1 + 2.0.1.Final + 2.24.0 1.3.1.Final - 1.11.3 - 2.5.10.Final + 1.12.0 + 2.6.4.Final 0.1.18.Final - 1.19.8 - 3.3.6 + 1.20.1 + 3.4.0 - 2.0.0 - 1.4.5 - 2.7 - 2.4 + 2.0.2 + 1.4.7 + 2.8.2 + 2.6 2.4.0 - 6.9.0.202403050737-r + 6.10.0.202406032230-r 0.15.0 - 9.39.1 + 9.41.1 0.9.6 - 0.0.6 + 0.0.12 0.1.3 2.12.1 0.8.11 1.1.0 - 3.0.0 + 3.3.0 2.12.3 0.16.0 - 1.0.10 + 1.0.11 @@ -297,6 +291,12 @@ ${brotli4j.version} + + io.smallrye.certs + smallrye-certificate-generator + ${smallrye-certificate-generator.version} + + com.fasterxml.jackson @@ -385,7 +385,6 @@ opensearch-testcontainers ${opensearch-testcontainers.version} - io.opentelemetry @@ -397,7 +396,7 @@ io.opentelemetry opentelemetry-bom-alpha - ${opentelemetry-alpha.version} + ${opentelemetry.version}-alpha pom import @@ -435,6 +434,11 @@ import pom + + org.jctools + jctools-core + ${jctools-core.version} + io.smallrye.reactive vertx-mutiny-clients-bom @@ -667,6 +671,17 @@ ${project.version} + + io.quarkus + quarkus-tls-registry + ${project.version} + + + io.quarkus + quarkus-tls-registry-deployment + ${project.version} + + @@ -694,6 +709,21 @@ quarkus-config-yaml-deployment ${project.version} + + io.quarkus + quarkus-cyclonedx + ${project.version} + + + io.quarkus + quarkus-cyclonedx-deployment + ${project.version} + + + io.quarkus + quarkus-cyclonedx-generator + ${project.version} + io.quarkus quarkus-datasource-common @@ -889,6 +919,11 @@ quarkus-oidc-client-deployment ${project.version} + + io.quarkus + quarkus-oidc-client-spi + ${project.version} + io.quarkus quarkus-resteasy-client-oidc-filter @@ -909,6 +944,16 @@ quarkus-rest-client-oidc-filter-deployment ${project.version} + + io.quarkus + quarkus-oidc-client-registration + ${project.version} + + + io.quarkus + quarkus-oidc-client-registration-deployment + ${project.version} + io.quarkus quarkus-oidc-client-graphql @@ -1009,6 +1054,66 @@ quarkus-flyway-deployment ${project.version} + + io.quarkus + quarkus-flyway-postgresql + ${project.version} + + + io.quarkus + quarkus-flyway-postgresql-deployment + ${project.version} + + + io.quarkus + quarkus-flyway-oracle + ${project.version} + + + io.quarkus + quarkus-flyway-oracle-deployment + ${project.version} + + + io.quarkus + quarkus-flyway-mysql + ${project.version} + + + io.quarkus + quarkus-flyway-mysql-deployment + ${project.version} + + + io.quarkus + quarkus-flyway-mssql + ${project.version} + + + io.quarkus + quarkus-flyway-mssql-deployment + ${project.version} + + + io.quarkus + quarkus-flyway-derby + ${project.version} + + + io.quarkus + quarkus-flyway-derby-deployment + ${project.version} + + + io.quarkus + quarkus-flyway-db2 + ${project.version} + + + io.quarkus + quarkus-flyway-db2-deployment + ${project.version} + io.quarkus quarkus-liquibase @@ -1886,6 +1991,16 @@ quarkus-smallrye-openapi-common-deployment ${project.version} + + io.quarkus + quarkus-load-shedding + ${project.version} + + + io.quarkus + quarkus-load-shedding-deployment + ${project.version} + io.quarkus quarkus-vertx @@ -1896,6 +2011,11 @@ quarkus-vertx-deployment ${project.version} + + io.quarkus + quarkus-vertx-deployment-spi + ${project.version} + io.quarkus quarkus-vertx-latebound-mdc-provider @@ -1941,6 +2061,16 @@ quarkus-vertx-http-dev-ui-resources ${project.version} + + io.quarkus + quarkus-vertx-kotlin + ${project.version} + + + io.quarkus + quarkus-vertx-kotlin-deployment + ${project.version} + io.quarkus @@ -2073,6 +2203,16 @@ quarkus-grpc-stubs ${project.version} + + io.quarkus + quarkus-grpc-reflection + ${project.version} + + + io.quarkus + quarkus-grpc-cli + ${project.version} + io.quarkus quarkus-grpc-deployment @@ -2119,6 +2259,11 @@ quarkus-websockets-next-deployment ${project.version} + + io.quarkus + quarkus-websockets-next-kotlin + ${project.version} + io.quarkus quarkus-undertow-spi @@ -4892,12 +5037,23 @@ resteasy-reactive-client-processor ${project.version} + + io.quarkus.vertx.utils + quarkus-vertx-utils + ${project.version} + org.wildfly.common wildfly-common ${wildfly-common.version} + + org.cyclonedx + cyclonedx-core-java + ${cyclonedx.version} + + org.wildfly.openssl wildfly-openssl-java @@ -5007,6 +5163,11 @@ wildfly-elytron-x500-cert ${wildfly-elytron.version} + + org.wildfly.security + wildfly-elytron-x500-cert-acme + ${wildfly-elytron.version} + org.wildfly.security wildfly-elytron-credential @@ -5326,6 +5487,12 @@ + + + org.apache.derby + derbyshared + ${derby-jdbc.version} + org.apache.derby derbyclient @@ -5630,6 +5797,11 @@ + + io.smallrye.reactive + mutiny-zero + ${mutiny-zero.version} + io.smallrye.reactive mutiny-zero-flow-adapters @@ -5704,6 +5876,12 @@ + + io.cloudevents + cloudevents-api + ${cloudevents-api.version} + + com.microsoft.azure.functions azure-functions-java-library @@ -5881,6 +6059,11 @@ quarkus-spring-beans-api ${quarkus-spring-api.version} + + io.quarkus + quarkus-spring-aop-api + ${quarkus-spring-api.version} + io.quarkus quarkus-spring-data-jpa-api @@ -5922,18 +6105,6 @@ ${quarkus-spring-boot-api.version} - - - org.keycloak - keycloak-adapter-core - ${keycloak.version} - - - commons-logging - commons-logging - - - org.keycloak keycloak-core @@ -6224,6 +6395,7 @@ ${picocli.version} + org.hdrhistogram @@ -6324,59 +6496,26 @@ io.opentelemetry.semconv opentelemetry-semconv ${opentelemetry-semconv.version} - - - io.opentelemetry - opentelemetry-bom - - - io.opentelemetry - opentelemetry-api - - - - - - - io.quarkus - quarkus-opentelemetry-exporter-jaeger-deployment - ${project.version} - - - io.quarkus - quarkus-opentelemetry-exporter-jaeger - ${project.version} - - - io.quarkus - quarkus-opentelemetry-exporter-otlp-deployment - ${project.version} - - - io.quarkus - quarkus-opentelemetry-exporter-otlp - ${project.version} - io.quarkus - quarkus-jaeger - ${project.version} - - - io.quarkus - quarkus-jaeger-deployment - ${project.version} + io.opentelemetry.semconv + opentelemetry-semconv-incubating + ${opentelemetry-semconv.version} + + io.quarkus - quarkus-smallrye-opentracing + quarkus-jfr ${project.version} io.quarkus - quarkus-smallrye-opentracing-deployment + quarkus-jfr-deployment ${project.version} + + io.quarkus quarkus-hibernate-search-orm-coordination-outbox-polling @@ -6387,369 +6526,7 @@ quarkus-hibernate-search-orm-coordination-outbox-polling-deployment ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-kotlin-serialization - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-kotlin-serialization-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-common - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-common-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-kotlin - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-kotlin-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-spi-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-server-spi-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-servlet-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-servlet - ${project.version} - - - io.quarkus - quarkus-jaxrs-client-reactive - ${project.version} - - - io.quarkus - quarkus-jaxrs-client-reactive-deployment - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-deployment - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-spi-deployment - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-jackson - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-jackson-deployment - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-jaxb - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-jaxb-deployment - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-jsonb - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-jsonb-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-kotlin-serialization-common - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-kotlin-serialization-common-deployment - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-kotlin-serialization - ${project.version} - - - io.quarkus - quarkus-rest-client-reactive-kotlin-serialization-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-qute - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-qute-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jsonb - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jsonb-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jsonb-common - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jsonb-common-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jaxb - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jaxb-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jackson - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jackson-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jackson-common - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-jackson-common-deployment - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-links - ${project.version} - - - io.quarkus - quarkus-resteasy-reactive-links-deployment - ${project.version} - - - io.quarkus - quarkus-csrf-reactive - ${project.version} - - - io.quarkus - quarkus-csrf-reactive-deployment - ${project.version} - - - io.quarkus - quarkus-oidc-token-propagation-reactive - ${project.version} - - - io.quarkus - quarkus-oidc-token-propagation-reactive-deployment - ${project.version} - - - io.quarkus - quarkus-oidc-token-propagation - ${project.version} - - - io.quarkus - quarkus-oidc-token-propagation-deployment - ${project.version} - - - io.quarkus - quarkus-oidc-client-filter - ${project.version} - - - io.quarkus - quarkus-oidc-client-filter-deployment - ${project.version} - - - io.quarkus - quarkus-oidc-client-reactive-filter - ${project.version} - - - io.quarkus - quarkus-oidc-client-reactive-filter-deployment - ${project.version} - - - io.quarkus - quarkus-keycloak-admin-client - ${project.version} - - - io.quarkus - quarkus-keycloak-admin-client-deployment - ${project.version} - - - io.quarkus - quarkus-keycloak-admin-client-reactive - ${project.version} - - - io.quarkus - quarkus-keycloak-admin-client-reactive-deployment - ${project.version} - - - io.quarkus - quarkus-spring-web-resteasy-reactive - ${project.version} - - - io.quarkus - quarkus-spring-web-resteasy-reactive-deployment - ${project.version} - - - io.quarkus - quarkus-spring-web-resteasy-classic - ${project.version} - - - io.quarkus - quarkus-spring-web-resteasy-classic-deployment - ${project.version} - - - - io.quarkus - quarkus-smallrye-reactive-messaging - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-kotlin - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-deployment - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-kafka - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-kafka-deployment - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-pulsar - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-pulsar-deployment - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-amqp - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-amqp-deployment - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-mqtt - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-mqtt-deployment - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-rabbitmq - ${project.version} - - - io.quarkus - quarkus-smallrye-reactive-messaging-rabbitmq-deployment - ${project.version} - io.quarkus quarkus-webjars-locator diff --git a/bom/dev-ui/pom.xml b/bom/dev-ui/pom.xml index 8c304dd392210d..275548b3c94c5e 100644 --- a/bom/dev-ui/pom.xml +++ b/bom/dev-ui/pom.xml @@ -13,11 +13,11 @@ Dependency management for dev-ui. Importable by third party extension developers. - 24.3.13 - 3.1.3 - 4.0.5 - 3.1.3 - 1.2.0 + 24.4.9 + 3.2.0 + 4.1.0 + 3.2.0 + 1.2.1 2.0.7 2.0.4 2.1.2 @@ -27,11 +27,11 @@ 1.4.0 1.7.5 1.7.0 - 5.5.0 + 5.5.1 1.10.0 2.4.0 - 1.0.16 - 1.0.0 + 1.0.17 + 1.0.1 2.15.3 17.7.2 @@ -269,6 +269,49 @@ runtime + + + org.mvnpm + markdown-it + 14.1.0 + runtime + + + org.mvnpm + argparse + 2.0.1 + runtime + + + org.mvnpm + entities + 4.5.0 + runtime + + + org.mvnpm + linkify-it + 5.0.0 + runtime + + + org.mvnpm + mdurl + 2.0.0 + runtime + + + org.mvnpm + punycode.js + 2.3.1 + runtime + + + org.mvnpm + uc.micro + 2.1.0 + runtime + org.mvnpm.at.mvnpm diff --git a/bom/test/pom.xml b/bom/test/pom.xml index 242cdea62dc0ba..91b39861d8ace0 100644 --- a/bom/test/pom.xml +++ b/bom/test/pom.xml @@ -20,7 +20,7 @@ 2.3.1 1.3.8 - 0.100.0 + 0.107.0 1.0.0-alpha @@ -42,6 +42,13 @@ ${rxjava1.version} + + io.smallrye.certs + smallrye-certificate-generator-junit5 + ${smallrye-certificate-generator.version} + test + + io.strimzi strimzi-test-container diff --git a/build-parent/pom.xml b/build-parent/pom.xml index a997b9305eeac7..65aa987db0d5c8 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -19,32 +19,28 @@ - 3.12.1 - 2.0.0 + ${version.compiler.plugin} + 2.0.20 1.9.20 2.13.12 - 4.9.1 + 4.9.2 ${scala-maven-plugin.version} - - 3.2.1 - 3.2.5 - 3.0.0 - ${version.surefire.plugin} + 1.6.Final - 3.2.0 + 3.2.2 1.0.0 2.5.13 - 4.1.0 - 3.25.10 + 4.3.0 + 3.26.2 2.0.3.Final 6.0.1 - 2.3.32 - 0.22.0 + 2.3.33 + 0.23.0 1.7.0.Final @@ -59,36 +55,29 @@ - [${maven.min.version},) + + [3.8.6,) - 3.9.6 - 3.2.0 - 8.7 + 3.9.9 + 3.3.2 + 8.9 ${project.version} ${project.version} 3.8.1 - - 6.4.4.Final - - 4.13.0 - :Z - 8.13.2 + 8.15.0 docker.io/elastic/elasticsearch:${elasticsearch-server.version} docker.io/elastic/logstash:${elasticsearch-server.version} docker.io/elastic/kibana:${elasticsearch-server.version} http - 2.13.0 + 2.16.0 docker.io/opensearchproject/opensearch:${opensearch-server.version} http 2.2.0 @@ -98,41 +87,36 @@ docker.io/mariadb:10.11 icr.io/db2_community/db2:11.5.9.0 mcr.microsoft.com/mssql/server:2022-latest - docker.io/mysql:8.0 + docker.io/mysql:8.4 docker.io/gvenzl/oracle-free:23-slim-faststart - docker.io/mongo:4.4 + docker.io/mongo:7.0 4.13.2 - 24.0.4 + 25.0.6 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy - 7.0.0 + 7.0.1 - 3.25.3 + 3.26.3 - 3.6.0 + 3.9.1 7.3.0 - 2.33.0 + 2.37.0 2.0.0 - 0.44.0 - 2.23.0 - 1.9.0 - 3.6.1 - 3.1.0 - 3.1.2 - 3.0.0 + 0.45.0 + 3.8.0 0.14.7 @@ -384,9 +368,9 @@ - me.escoffier.certs - certificate-generator-junit5 - 0.5.0 + io.smallrye.certs + smallrye-certificate-generator-junit5 + ${smallrye-certificate-generator.version} test @@ -414,7 +398,6 @@ maven-compiler-plugin - ${compiler-plugin.version} org.codehaus.mojo @@ -438,7 +421,6 @@ maven-surefire-plugin - ${version.surefire.plugin} @@ -457,13 +439,13 @@ - ${jacoco.agent.argLine} -Xmx1500m -XX:MaxMetaspaceSize=1500m -Djava.io.tmpdir="${project.build.directory}" ${surefire.argLine.additional} + + ${jacoco.agent.argLine} -Xmx1500m -XX:MaxMetaspaceSize=1500m -Djava.io.tmpdir="${project.build.directory}" ${surefire.argLine.additional} --add-opens java.naming/com.sun.naming.internal=ALL-UNNAMED MAVEN_OPTS maven-failsafe-plugin - ${failsafe-plugin.version} @@ -484,11 +466,6 @@ MAVEN_OPTS - - org.apache.maven.plugins - maven-dependency-plugin - ${maven-dependency-plugin.version} - io.quarkus quarkus-maven-plugin @@ -525,6 +502,7 @@ enforce + ${maven-enforcer-plugin.phase} @@ -563,41 +541,8 @@ jandex-maven-plugin ${jandex.version} - - net.revelc.code.formatter - formatter-maven-plugin - ${formatter-maven-plugin.version} - - - quarkus-ide-config - io.quarkus - ${project.version} - - - - - .cache/formatter-maven-plugin-${formatter-maven-plugin.version} - eclipse-format.xml - LF - ${format.skip} - - - - net.revelc.code - impsort-maven-plugin - ${impsort-maven-plugin.version} - - - .cache/impsort-maven-plugin-${impsort-maven-plugin.version} - java.,javax.,jakarta.,org.,com. - * - ${format.skip} - true - - maven-resources-plugin - ${maven-resources-plugin.version} eot @@ -653,7 +598,7 @@ org.apache.groovy groovy - 4.0.21 + 4.0.23 @@ -679,7 +624,7 @@ true **.SuppressForbidden - compile + ${forbiddenapis-maven-plugin.phase} check @@ -701,7 +646,20 @@ - + + org.jboss.bridger + bridger + ${jboss-bridger-plugin.version} + + + weave + process-classes + + transform + + + + diff --git a/core/builder/src/main/java/io/quarkus/builder/Json.java b/core/builder/src/main/java/io/quarkus/builder/Json.java index 1973e3a0411c1f..57a55a30773899 100644 --- a/core/builder/src/main/java/io/quarkus/builder/Json.java +++ b/core/builder/src/main/java/io/quarkus/builder/Json.java @@ -11,6 +11,15 @@ import java.util.Map.Entry; import java.util.Objects; +import io.quarkus.builder.json.JsonArray; +import io.quarkus.builder.json.JsonBoolean; +import io.quarkus.builder.json.JsonInteger; +import io.quarkus.builder.json.JsonMember; +import io.quarkus.builder.json.JsonMultiValue; +import io.quarkus.builder.json.JsonObject; +import io.quarkus.builder.json.JsonString; +import io.quarkus.builder.json.JsonValue; + /** * A simple JSON string generator. */ @@ -48,7 +57,7 @@ private Json() { * @return the new JSON array builder, empty builders are not ignored */ public static JsonArrayBuilder array() { - return new JsonArrayBuilder(false); + return new JsonArrayBuilder(false, false); } /** @@ -56,15 +65,15 @@ public static JsonArrayBuilder array() { * @return the new JSON array builder * @see JsonBuilder#ignoreEmptyBuilders */ - static JsonArrayBuilder array(boolean ignoreEmptyBuilders) { - return new JsonArrayBuilder(ignoreEmptyBuilders); + public static JsonArrayBuilder array(boolean ignoreEmptyBuilders, boolean skipEscapeCharacters) { + return new JsonArrayBuilder(ignoreEmptyBuilders, skipEscapeCharacters); } /** * @return the new JSON object builder, empty builders are not ignored */ public static JsonObjectBuilder object() { - return new JsonObjectBuilder(false); + return new JsonObjectBuilder(false, false); } /** @@ -72,20 +81,30 @@ public static JsonObjectBuilder object() { * @return the new JSON object builder * @see JsonBuilder#ignoreEmptyBuilders */ - static JsonObjectBuilder object(boolean ignoreEmptyBuilders) { - return new JsonObjectBuilder(ignoreEmptyBuilders); + public static JsonObjectBuilder object(boolean ignoreEmptyBuilders, boolean skipEscapeCharacters) { + return new JsonObjectBuilder(ignoreEmptyBuilders, skipEscapeCharacters); } - abstract static class JsonBuilder { + public abstract static class JsonBuilder { - protected boolean ignoreEmptyBuilders = false; + protected final boolean ignoreEmptyBuilders; + /** + * Skips escaping characters in string values. + * This option should be enabled when transforming JSON input, + * whose string values are already escaped. + * In situations like this, the option avoids escaping characters + * that are already escaped. + */ + protected final boolean skipEscapeCharacters; + protected JsonTransform transform; /** * @param ignoreEmptyBuilders If set to true all empty builders added to this builder will be ignored during * {@link #build()} */ - JsonBuilder(boolean ignoreEmptyBuilders) { + JsonBuilder(boolean ignoreEmptyBuilders, boolean skipEscapeCharacters) { this.ignoreEmptyBuilders = ignoreEmptyBuilders; + this.skipEscapeCharacters = skipEscapeCharacters; } /** @@ -130,6 +149,16 @@ protected boolean isValuesEmpty(Collection values) { protected abstract T self(); + abstract void add(JsonValue element); + + void setTransform(JsonTransform transform) { + this.transform = transform; + } + + public void transform(JsonMultiValue value, JsonTransform transform) { + final ResolvedTransform resolved = new ResolvedTransform(this, transform); + value.forEach(resolved); + } } /** @@ -139,8 +168,8 @@ public static class JsonArrayBuilder extends JsonBuilder { private final List values; - private JsonArrayBuilder(boolean ignoreEmptyBuilders) { - super(ignoreEmptyBuilders); + private JsonArrayBuilder(boolean ignoreEmptyBuilders, boolean skipEscapeCharacters) { + super(ignoreEmptyBuilders, skipEscapeCharacters); this.values = new ArrayList(); } @@ -209,7 +238,7 @@ public void appendTo(Appendable appendable) throws IOException { if (++idx > 1) { appendable.append(ENTRY_SEPARATOR); } - appendValue(appendable, value); + appendValue(appendable, value, skipEscapeCharacters); } appendable.append(ARRAY_END); } @@ -219,6 +248,32 @@ protected JsonArrayBuilder self() { return this; } + @Override + void add(JsonValue element) { + if (element instanceof JsonString) { + add(((JsonString) element).value()); + } else if (element instanceof JsonInteger) { + final long longValue = ((JsonInteger) element).longValue(); + final int intValue = (int) longValue; + if (longValue == intValue) { + add(intValue); + } else { + add(longValue); + } + } else if (element instanceof JsonBoolean) { + add(((JsonBoolean) element).value()); + } else if (element instanceof JsonArray) { + final JsonArrayBuilder arrayBuilder = Json.array(ignoreEmptyBuilders, skipEscapeCharacters); + arrayBuilder.transform((JsonArray) element, transform); + add(arrayBuilder); + } else if (element instanceof JsonObject) { + final JsonObjectBuilder objectBuilder = Json.object(ignoreEmptyBuilders, skipEscapeCharacters); + objectBuilder.transform((JsonObject) element, transform); + if (!objectBuilder.isEmpty()) { + add(objectBuilder); + } + } + } } /** @@ -228,8 +283,8 @@ public static class JsonObjectBuilder extends JsonBuilder { private final Map properties; - private JsonObjectBuilder(boolean ignoreEmptyBuilders) { - super(ignoreEmptyBuilders); + private JsonObjectBuilder(boolean ignoreEmptyBuilders, boolean skipEscapeCharacters) { + super(ignoreEmptyBuilders, skipEscapeCharacters); this.properties = new HashMap(); } @@ -299,9 +354,9 @@ public void appendTo(Appendable appendable) throws IOException { if (++idx > 1) { appendable.append(ENTRY_SEPARATOR); } - appendStringValue(appendable, entry.getKey()); + appendStringValue(appendable, entry.getKey(), skipEscapeCharacters); appendable.append(NAME_VAL_SEPARATOR); - appendValue(appendable, entry.getValue()); + appendValue(appendable, entry.getValue(), skipEscapeCharacters); } appendable.append(OBJECT_END); } @@ -311,15 +366,47 @@ protected JsonObjectBuilder self() { return this; } + @Override + void add(JsonValue element) { + if (element instanceof JsonMember) { + final JsonMember member = (JsonMember) element; + final String attribute = member.attribute().value(); + final JsonValue value = member.value(); + if (value instanceof JsonString) { + put(attribute, ((JsonString) value).value()); + } else if (value instanceof JsonInteger) { + final long longValue = ((JsonInteger) value).longValue(); + final int intValue = (int) longValue; + if (longValue == intValue) { + put(attribute, intValue); + } else { + put(attribute, longValue); + } + } else if (value instanceof JsonBoolean) { + final boolean booleanValue = ((JsonBoolean) value).value(); + put(attribute, booleanValue); + } else if (value instanceof JsonArray) { + final JsonArrayBuilder arrayBuilder = Json.array(ignoreEmptyBuilders, skipEscapeCharacters); + arrayBuilder.transform((JsonArray) value, transform); + put(attribute, arrayBuilder); + } else if (value instanceof JsonObject) { + final JsonObjectBuilder objectBuilder = Json.object(ignoreEmptyBuilders, skipEscapeCharacters); + objectBuilder.transform((JsonObject) value, transform); + if (!objectBuilder.isEmpty()) { + put(attribute, objectBuilder); + } + } + } + } } - static void appendValue(Appendable appendable, Object value) throws IOException { + static void appendValue(Appendable appendable, Object value, boolean skipEscapeCharacters) throws IOException { if (value instanceof JsonObjectBuilder) { appendable.append(((JsonObjectBuilder) value).build()); } else if (value instanceof JsonArrayBuilder) { appendable.append(((JsonArrayBuilder) value).build()); } else if (value instanceof String) { - appendStringValue(appendable, value.toString()); + appendStringValue(appendable, value.toString(), skipEscapeCharacters); } else if (value instanceof Boolean || value instanceof Integer || value instanceof Long) { appendable.append(value.toString()); } else { @@ -327,9 +414,13 @@ static void appendValue(Appendable appendable, Object value) throws IOException } } - static void appendStringValue(Appendable appendable, String value) throws IOException { + static void appendStringValue(Appendable appendable, String value, boolean skipEscapeCharacters) throws IOException { appendable.append(CHAR_QUOTATION_MARK); - appendable.append(escape(value)); + if (skipEscapeCharacters) { + appendable.append(value); + } else { + appendable.append(escape(value)); + } appendable.append(CHAR_QUOTATION_MARK); } @@ -354,4 +445,21 @@ static String escape(String value) { return builder.toString(); } + private static final class ResolvedTransform implements JsonTransform { + private final Json.JsonBuilder resolvedBuilder; + private final JsonTransform transform; + + private ResolvedTransform(Json.JsonBuilder resolvedBuilder, JsonTransform transform) { + this.resolvedBuilder = resolvedBuilder; + this.resolvedBuilder.setTransform(transform); + this.transform = transform; + } + + @Override + public void accept(Json.JsonBuilder builder, JsonValue element) { + if (builder == null) { + transform.accept(resolvedBuilder, element); + } + } + } } diff --git a/core/builder/src/main/java/io/quarkus/builder/JsonReader.java b/core/builder/src/main/java/io/quarkus/builder/JsonReader.java new file mode 100644 index 00000000000000..b0fc9f51a01e6a --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/JsonReader.java @@ -0,0 +1,351 @@ +package io.quarkus.builder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.builder.json.JsonArray; +import io.quarkus.builder.json.JsonBoolean; +import io.quarkus.builder.json.JsonDouble; +import io.quarkus.builder.json.JsonInteger; +import io.quarkus.builder.json.JsonNull; +import io.quarkus.builder.json.JsonObject; +import io.quarkus.builder.json.JsonString; +import io.quarkus.builder.json.JsonValue; + +/** + * A json format reader. + * It follows the ECMA-404 The JSON Data Interchange Standard.. + */ +public class JsonReader { + + private final String text; + private final int length; + private int position; + + private JsonReader(String text) { + this.text = text; + this.length = text.length(); + } + + public static JsonReader of(String source) { + return new JsonReader(source); + } + + @SuppressWarnings("unchecked") + public T read() { + return (T) readElement(); + } + + /** + * element + * |---- ws value ws + */ + private JsonValue readElement() { + ignoreWhitespace(); + JsonValue result = readValue(); + ignoreWhitespace(); + return result; + } + + /** + * value + * |---- object + * |---- array + * |---- string + * |---- number + * |---- "true" + * |---- "false" + * |---- "null" + */ + private JsonValue readValue() { + final int ch = peekChar(); + if (ch < 0) { + throw new IllegalArgumentException("Unable to fully read json value"); + } + + switch (ch) { + case '{': + return readObject(); + case '[': + return readArray(); + case '"': + return readString(); + case 't': + return readConstant("true", JsonBoolean.TRUE); + case 'f': + return readConstant("false", JsonBoolean.FALSE); + case 'n': + return readConstant("null", JsonNull.INSTANCE); + default: + if (Character.isDigit(ch) || '-' == ch) { + return readNumber(position); + } + throw new IllegalArgumentException("Unknown start character for json value: " + ch); + } + } + + /** + * object + * |---- '{' ws '}' + * |---- '{' members '}' + *

+ * members + * |----- member + * |----- member ',' members + */ + private JsonValue readObject() { + position++; + + Map members = new HashMap<>(); + + while (position < length) { + ignoreWhitespace(); + switch (peekChar()) { + case '}': + position++; + return new JsonObject(members); + case ',': + position++; + break; + case '"': + readMember(members); + break; + } + } + + throw new IllegalArgumentException("Json object ended without }"); + } + + /** + * member + * |----- ws string ws ':' element + */ + private void readMember(Map members) { + final JsonString attribute = readString(); + ignoreWhitespace(); + final int colon = nextChar(); + if (':' != colon) { + throw new IllegalArgumentException("Expected : after attribute"); + } + final JsonValue element = readElement(); + members.put(attribute, element); + } + + /** + * array + * |---- '[' ws ']' + * |---- '[' elements ']' + *

+ * elements + * |----- element + * |----- element ',' elements + */ + private JsonValue readArray() { + position++; + + final List elements = new ArrayList<>(); + + while (position < length) { + ignoreWhitespace(); + switch (peekChar()) { + case ']': + position++; + return new JsonArray(elements); + case ',': + position++; + break; + default: + elements.add(readElement()); + break; + } + } + + throw new IllegalArgumentException("Json array ended without ]"); + } + + /** + * string + * |---- '"' characters '"' + *

+ * characters + * |----- "" + * |----- character characters + *

+ * character + * |----- '0020' . '10FFFF' - '"' - '\' + * |----- '\' escape + * |----- escape + * |----- '"' + * |----- '\' + * |----- '/' + * |----- 'b' + * |----- 'f' + * |----- 'n' + * |----- 'r' + * |----- 't' + * |----- 'u' hex hex hex hex + */ + private JsonString readString() { + position++; + + int start = position; + // Substring on string values that contain unicode characters won't work, + // because there are more characters read than actual characters represented. + // Use StringBuilder to buffer any string read up to unicode, + // then add unicode values into it and continue as usual. + StringBuilder unicodeString = null; + + while (position < length) { + final int ch = nextChar(); + + if (Character.isISOControl(ch)) { + throw new IllegalArgumentException("Control characters not allowed in json string"); + } + + if ('"' == ch) { + final String chunk = text.substring(start, position - 1); + final String result = unicodeString != null + ? unicodeString.append(chunk).toString() + : chunk; + + // End of string + return new JsonString(result); + } + + if ('\\' == ch) { + switch (nextChar()) { + case '"': // quotation mark + case '\\': // reverse solidus + case '/': // solidus + case 'b': // backspace + case 'f': // formfeed + case 'n': // linefeed + case 'r': // carriage return + case 't': // horizontal tab + break; + case 'u': // unicode + if (unicodeString == null) { + unicodeString = new StringBuilder(position - start); + } + unicodeString.append(text, start, position - 1); + unicodeString.append(readUnicode()); + start = position; + } + } + } + + throw new IllegalArgumentException("String not closed"); + } + + private char readUnicode() { + final char digit1 = Character.forDigit(nextChar(), 16); + final char digit2 = Character.forDigit(nextChar(), 16); + final char digit3 = Character.forDigit(nextChar(), 16); + final char digit4 = Character.forDigit(nextChar(), 16); + return (char) (digit1 << 12 | digit2 << 8 | digit3 << 4 | digit4); + } + + /** + * number + * |---- integer fraction exponent + */ + private JsonValue readNumber(int numStartIndex) { + final boolean isFraction = skipToEndOfNumber(); + final String number = text.substring(numStartIndex, position); + return isFraction + ? new JsonDouble(Double.parseDouble(number)) + : new JsonInteger(Long.parseLong(number)); + } + + private boolean skipToEndOfNumber() { + // Find the end of a number then parse with library methods + int ch = nextChar(); + if ('-' == ch) { + ch = nextChar(); + } + + if (Character.isDigit(ch) && '0' != ch) { + ignoreDigits(); + } + + boolean isFraction = false; + ch = peekChar(); + if ('.' == ch) { + isFraction = true; + position++; + ignoreDigits(); + } + + ch = peekChar(); + switch (ch) { + case 'e': + case 'E': + position++; + ch = nextChar(); + switch (ch) { + case '-': + case '+': + position++; + } + ignoreDigits(); + } + + return isFraction; + } + + private void ignoreDigits() { + while (position < length) { + final int ch = peekChar(); + if (!Character.isDigit(ch)) { + break; + } + position++; + } + } + + private JsonValue readConstant(String expected, JsonValue result) { + if (text.regionMatches(position, expected, 0, expected.length())) { + position += expected.length(); + return result; + } + throw new IllegalArgumentException("Unable to read json constant for: " + expected); + } + + /** + * ws + * |---- "" + * |---- '0020' ws + * |---- '000A' ws + * |---- '000D' ws + * |---- '0009' ws + */ + private void ignoreWhitespace() { + while (position < length) { + final int ch = peekChar(); + switch (ch) { + case ' ': // '0020' SPACE + case '\n': // '000A' LINE FEED + case '\r': // '000D' CARRIAGE RETURN + case '\t': // '0009' CHARACTER TABULATION + position++; + break; + default: + return; + } + } + } + + private int peekChar() { + return position < length + ? text.charAt(position) + : -1; + } + + private int nextChar() { + final int ch = peekChar(); + position++; + return ch; + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/JsonTransform.java b/core/builder/src/main/java/io/quarkus/builder/JsonTransform.java new file mode 100644 index 00000000000000..c6994add0ed699 --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/JsonTransform.java @@ -0,0 +1,17 @@ +package io.quarkus.builder; + +import java.util.function.Predicate; + +import io.quarkus.builder.json.JsonValue; + +@FunctionalInterface +public interface JsonTransform { + void accept(Json.JsonBuilder builder, JsonValue element); + + static JsonTransform dropping(Predicate filter) { + return (builder, element) -> { + if (!filter.test(element)) + builder.add(element); + }; + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonArray.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonArray.java new file mode 100644 index 00000000000000..f88c4f9feb89aa --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonArray.java @@ -0,0 +1,28 @@ +package io.quarkus.builder.json; + +import java.util.List; +import java.util.stream.Stream; + +import io.quarkus.builder.JsonTransform; + +public final class JsonArray implements JsonMultiValue { + private final List value; + + public JsonArray(List value) { + this.value = value; + } + + public List value() { + return value; + } + + @SuppressWarnings("unchecked") + public Stream stream() { + return (Stream) value.stream(); + } + + @Override + public void forEach(JsonTransform transform) { + value.forEach(v -> transform.accept(null, v)); + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonBoolean.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonBoolean.java new file mode 100644 index 00000000000000..da01cf9aa28cca --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonBoolean.java @@ -0,0 +1,16 @@ +package io.quarkus.builder.json; + +public enum JsonBoolean implements JsonValue { + TRUE(true), + FALSE(false); + + private final boolean value; + + JsonBoolean(boolean value) { + this.value = value; + } + + public boolean value() { + return value; + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonDouble.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonDouble.java new file mode 100644 index 00000000000000..8a567401f829aa --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonDouble.java @@ -0,0 +1,13 @@ +package io.quarkus.builder.json; + +public final class JsonDouble implements JsonNumber { + private final double value; + + public JsonDouble(double value) { + this.value = value; + } + + public double value() { + return value; + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonInteger.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonInteger.java new file mode 100644 index 00000000000000..062d613de74111 --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonInteger.java @@ -0,0 +1,17 @@ +package io.quarkus.builder.json; + +public final class JsonInteger implements JsonNumber { + private final long value; + + public JsonInteger(long value) { + this.value = value; + } + + public long longValue() { + return value; + } + + public int intValue() { + return (int) value; + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonMember.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonMember.java new file mode 100644 index 00000000000000..1ce4b2a88797c2 --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonMember.java @@ -0,0 +1,19 @@ +package io.quarkus.builder.json; + +public final class JsonMember implements JsonValue { + private final JsonString attribute; + private final JsonValue value; + + public JsonMember(JsonString attribute, JsonValue value) { + this.attribute = attribute; + this.value = value; + } + + public JsonString attribute() { + return attribute; + } + + public JsonValue value() { + return value; + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonMultiValue.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonMultiValue.java new file mode 100644 index 00000000000000..42ce6b88d992c6 --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonMultiValue.java @@ -0,0 +1,9 @@ +package io.quarkus.builder.json; + +import io.quarkus.builder.JsonTransform; + +public interface JsonMultiValue extends JsonValue { + default void forEach(JsonTransform transform) { + transform.accept(null, this); + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonNull.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonNull.java new file mode 100644 index 00000000000000..13c8d995bc643b --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonNull.java @@ -0,0 +1,5 @@ +package io.quarkus.builder.json; + +public enum JsonNull implements JsonValue { + INSTANCE; +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonNumber.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonNumber.java new file mode 100644 index 00000000000000..2c25838dca80be --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonNumber.java @@ -0,0 +1,4 @@ +package io.quarkus.builder.json; + +public interface JsonNumber extends JsonValue { +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonObject.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonObject.java new file mode 100644 index 00000000000000..6718d8687ad602 --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonObject.java @@ -0,0 +1,31 @@ +package io.quarkus.builder.json; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.quarkus.builder.JsonTransform; + +public final class JsonObject implements JsonMultiValue { + private final Map value; + + public JsonObject(Map value) { + this.value = value; + } + + @SuppressWarnings("unchecked") + public T get(String attribute) { + return (T) value.get(new JsonString(attribute)); + } + + public List members() { + return value.entrySet().stream() + .map(e -> new JsonMember(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + + @Override + public void forEach(JsonTransform transform) { + members().forEach(member -> transform.accept(null, member)); + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonString.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonString.java new file mode 100644 index 00000000000000..5f13517539e05d --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonString.java @@ -0,0 +1,30 @@ +package io.quarkus.builder.json; + +import java.util.Objects; + +public final class JsonString implements JsonValue { + private final String value; + + public JsonString(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + JsonString that = (JsonString) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/core/builder/src/main/java/io/quarkus/builder/json/JsonValue.java b/core/builder/src/main/java/io/quarkus/builder/json/JsonValue.java new file mode 100644 index 00000000000000..a990951d5679b2 --- /dev/null +++ b/core/builder/src/main/java/io/quarkus/builder/json/JsonValue.java @@ -0,0 +1,4 @@ +package io.quarkus.builder.json; + +public interface JsonValue { +} diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index 367713725caf60..b0efea90680a8e 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -147,34 +147,21 @@ org.junit.jupiter junit-jupiter + + org.bouncycastle + bcpkix-jdk18on + test + + + org.jboss.shrinkwrap + shrinkwrap-depchain + test + pom + - - maven-dependency-plugin - - - download-signed-jar - generate-test-resources - - copy - - - - - org.eclipse.jgit - org.eclipse.jgit.ssh.apache - 6.9.0.202403050737-r - jar - signed.jar - - - ${project.build.testOutputDirectory} - - - - maven-surefire-plugin @@ -190,6 +177,7 @@ enforce + ${maven-enforcer-plugin.phase} enforce @@ -226,15 +214,20 @@ maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + de.thetaphi diff --git a/core/deployment/src/main/java/io/quarkus/banner/BannerConfig.java b/core/deployment/src/main/java/io/quarkus/banner/BannerConfig.java index ae5d02030e8301..ce1d2ddf0f9717 100644 --- a/core/deployment/src/main/java/io/quarkus/banner/BannerConfig.java +++ b/core/deployment/src/main/java/io/quarkus/banner/BannerConfig.java @@ -3,6 +3,9 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Banner + */ @ConfigRoot(name = "banner") public class BannerConfig { 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 fc4e1e776034c0..081810130ed904 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java @@ -4,6 +4,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** + * Bootstrap + *

* This is used currently only to suppress warnings about unknown properties * when the user supplies something like: -Dquarkus.debug.reflection=true */ @@ -42,7 +44,7 @@ public class BootstrapConfig { boolean disableJarCache; /** - * A temporary option introduced to avoid a logging warning when {@code }-Dquarkus.bootstrap.incubating-model-resolver} + * A temporary option introduced to avoid a logging warning when {@code -Dquarkus.bootstrap.incubating-model-resolver} * is added to the build command line. * This option enables an incubating implementation of the Quarkus Application Model resolver. * This option will be removed as soon as the incubating implementation becomes the default one. diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java index 92abc57bd3e642..c98dad86571c4d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java @@ -1,6 +1,6 @@ package io.quarkus.deployment; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.BufferedWriter; import java.io.IOException; @@ -227,7 +227,8 @@ public static void dumpCurrentConfigValues(ApplicationModel appModel, String lau if (previouslyRecordedProperties.isEmpty()) { try { readConfig(appModel, mode, buildSystemProps, deploymentClassLoader, configReader -> { - var config = configReader.initConfiguration(mode, buildSystemProps, appModel.getPlatformProperties()); + var config = configReader.initConfiguration(mode, buildSystemProps, new Properties(), + appModel.getPlatformProperties()); final Map allProps = new HashMap<>(); for (String name : config.getPropertyNames()) { allProps.put(name, ConfigTrackingValueTransformer.asString(config.getConfigValue(name))); @@ -287,7 +288,8 @@ public static void dumpCurrentConfigValues(ApplicationModel appModel, String lau public static Config getConfig(ApplicationModel appModel, LaunchMode launchMode, Properties buildSystemProps, QuarkusClassLoader deploymentClassLoader) throws CodeGenException { return readConfig(appModel, launchMode, buildSystemProps, deploymentClassLoader, - configReader -> configReader.initConfiguration(launchMode, buildSystemProps, appModel.getPlatformProperties())); + configReader -> configReader.initConfiguration(launchMode, buildSystemProps, new Properties(), + appModel.getPlatformProperties())); } public static T readConfig(ApplicationModel appModel, LaunchMode launchMode, Properties buildSystemProps, @@ -340,7 +342,7 @@ public static T readConfig(ApplicationModel appModel, LaunchMode launchMode, final QuarkusClassLoader.Builder configClBuilder = QuarkusClassLoader.builder("CodeGenerator Config ClassLoader", deploymentClassLoader, false); if (!allowedConfigServices.isEmpty()) { - configClBuilder.addElement(new MemoryClassPathElement(allowedConfigServices, true)); + configClBuilder.addNormalPriorityElement(new MemoryClassPathElement(allowedConfigServices, true)); } if (!bannedConfigServices.isEmpty()) { configClBuilder.addBannedElement(new MemoryClassPathElement(bannedConfigServices, true)); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CollectionClassProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/CollectionClassProcessor.java index ac0efab5551dcf..530a695ea74073 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CollectionClassProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CollectionClassProcessor.java @@ -8,13 +8,13 @@ public class CollectionClassProcessor { @BuildStep ReflectiveClassBuildItem setupCollectionClasses() { - return ReflectiveClassBuildItem.builder(ArrayList.class.getName(), - HashMap.class.getName(), - HashSet.class.getName(), - LinkedList.class.getName(), - LinkedHashMap.class.getName(), - LinkedHashSet.class.getName(), - TreeMap.class.getName(), - TreeSet.class.getName()).build(); + return ReflectiveClassBuildItem.builder(ArrayList.class, + HashMap.class, + HashSet.class, + LinkedList.class, + LinkedHashMap.class, + LinkedHashSet.class, + TreeMap.class, + TreeSet.class).reason(getClass().getName()).build(); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ConfigBuildTimeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/ConfigBuildTimeConfig.java index 95d1c4e04bdd44..059266a4ea3ba1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ConfigBuildTimeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ConfigBuildTimeConfig.java @@ -1,10 +1,15 @@ package io.quarkus.deployment; +import io.quarkus.runtime.annotations.ConfigDocPrefix; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Configuration + */ @ConfigRoot(name = ConfigItem.PARENT, phase = ConfigPhase.BUILD_TIME) +@ConfigDocPrefix("quarkus.config") public class ConfigBuildTimeConfig { /** *

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/DebugConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/DebugConfig.java index e75f81adc70576..d3a8130efc9c89 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/DebugConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/DebugConfig.java @@ -6,6 +6,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** + * Debugging + *

* This is used currently only to suppress warnings about unknown properties * when the user supplies something like: -Dquarkus.debug.reflection=true * diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java index d4076b42abab70..6d7bc5bd526c9b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java @@ -13,6 +13,8 @@ import static java.util.Arrays.asList; import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -23,6 +25,7 @@ import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -129,12 +132,14 @@ private static boolean isRecorder(AnnotatedElement element) { * @throws IOException if the class loader could not load a resource * @throws ClassNotFoundException if a build step class is not found */ - public static Consumer loadStepsFrom(ClassLoader classLoader, Properties buildSystemProps, + public static Consumer loadStepsFrom(ClassLoader classLoader, + Properties buildSystemProps, Properties runtimeProperties, ApplicationModel appModel, LaunchMode launchMode, DevModeType devModeType) throws IOException, ClassNotFoundException { final BuildTimeConfigurationReader reader = new BuildTimeConfigurationReader(classLoader); - final SmallRyeConfig src = reader.initConfiguration(launchMode, buildSystemProps, appModel.getPlatformProperties()); + final SmallRyeConfig src = reader.initConfiguration(launchMode, buildSystemProps, runtimeProperties, + appModel.getPlatformProperties()); // install globally QuarkusConfigFactory.setConfig(src); final BuildTimeConfigurationReader.ReadResult readResult = reader.readConfiguration(src); @@ -432,6 +437,7 @@ private static Consumer loadStepsFromClass(Class clazz, final List methods = getMethods(clazz); final Map> nameToMethods = methods.stream().collect(Collectors.groupingBy(m -> m.getName())); + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); for (Method method : methods) { final BuildStep buildStep = method.getAnnotation(BuildStep.class); if (buildStep == null) { @@ -785,6 +791,7 @@ private static Consumer loadStepsFromClass(Class clazz, stepId = name; } + MethodHandle methodHandle = unreflect(method, lookup); chainConfig = chainConfig .andThen(bcb -> { BuildStepBuilder bsb = bcb.addBuildStep(new io.quarkus.builder.BuildStep() { @@ -846,17 +853,13 @@ public void execute(final BuildContext bc) { } Object result; try { - result = method.invoke(instance, methodArgs); + result = methodHandle.bindTo(instance).invokeWithArguments(methodArgs); } catch (IllegalAccessException e) { throw ReflectUtil.toError(e); - } catch (InvocationTargetException e) { - try { - throw e.getCause(); - } catch (RuntimeException | Error e2) { - throw e2; - } catch (Throwable t) { - throw new IllegalStateException(t); - } + } catch (RuntimeException | Error e2) { + throw e2; + } catch (Throwable t) { + throw new UndeclaredThrowableException(t); } resultConsumer.accept(bc, result); if (isRecorder) { @@ -885,6 +888,15 @@ public String toString() { return chainConfig; } + private static MethodHandle unreflect(Method method, MethodHandles.Lookup lookup) { + try { + return lookup.unreflect(method); + } catch (IllegalAccessException e) { + throw ReflectUtil.toError(e); + } + + } + private static BooleanSupplier and(BooleanSupplier addStep, BooleanSupplierFactoryBuildItem supplierFactory, Class[] testClasses, boolean inv) { for (Class testClass : testClasses) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index b0c007df14313e..497136eab4c929 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -49,6 +49,7 @@ public enum Feature { JDBC_MSSQL, JDBC_MYSQL, JDBC_ORACLE, + JFR, KAFKA_CLIENT, KAFKA_STREAMS, KEYCLOAK_AUTHORIZATION, @@ -72,6 +73,7 @@ public enum Feature { OBSERVABILITY, OIDC, OIDC_CLIENT, + OIDC_CLIENT_REGISTRATION, RESTEASY_CLIENT_OIDC_FILTER, REST_CLIENT_OIDC_FILTER, OIDC_CLIENT_GRAPHQL_CLIENT_INTEGRATION, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java index eaff60bf8855aa..6f3547172cc3b0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java @@ -1,17 +1,13 @@ package io.quarkus.deployment; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; - import java.io.StringWriter; import java.io.Writer; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; import io.quarkus.bootstrap.BootstrapDebug; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; @@ -36,7 +32,7 @@ public GeneratedClassGizmoAdaptor(BuildProducer generat Predicate applicationClassPredicate) { this.generatedClasses = generatedClasses; this.applicationClassPredicate = applicationClassPredicate; - this.sources = BootstrapDebug.DEBUG_SOURCES_DIR != null ? new ConcurrentHashMap<>() : null; + this.sources = BootstrapDebug.debugSourcesDir() != null ? new ConcurrentHashMap<>() : null; } public GeneratedClassGizmoAdaptor(BuildProducer generatedClasses, @@ -48,7 +44,7 @@ public boolean test(String s) { return isApplicationClass(generatedToBaseNameFunction.apply(s)); } }; - this.sources = BootstrapDebug.DEBUG_SOURCES_DIR != null ? new ConcurrentHashMap<>() : null; + this.sources = BootstrapDebug.debugSourcesDir() != null ? new ConcurrentHashMap<>() : null; } @Override @@ -75,12 +71,7 @@ public Writer getSourceWriter(String className) { } public static boolean isApplicationClass(String className) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(fromClassNameToResourceName(className), true); - return !res.isEmpty(); + return QuarkusClassLoader.isApplicationClass(className); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/InetAddressProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/InetAddressProcessor.java new file mode 100644 index 00000000000000..2f9ae24552db1a --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/InetAddressProcessor.java @@ -0,0 +1,15 @@ +package io.quarkus.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; + +public class InetAddressProcessor { + + @BuildStep + void registerInetAddressServiceProvider(BuildProducer services) { + // service provider loaded by java.net.InetAddress.loadResolver + services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("java.net.spi.InetAddressResolverProvider")); + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java index 1efd20d2da3826..19882135e0e292 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -6,6 +6,7 @@ import java.util.List; import io.quarkus.deployment.util.ContainerRuntimeUtil; +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; public class IsDockerWorking extends IsContainerRuntimeWorking { public IsDockerWorking() { @@ -19,7 +20,8 @@ public IsDockerWorking(boolean silent) { private static class DockerBinaryStrategy implements Strategy { @Override public Result get() { - if (ContainerRuntimeUtil.detectContainerRuntime(false) != UNAVAILABLE) { + if (ContainerRuntimeUtil.detectContainerRuntime(false, + ContainerRuntime.DOCKER, ContainerRuntime.PODMAN) != UNAVAILABLE) { return Result.AVAILABLE; } else { return Result.UNKNOWN; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsPodmanWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsPodmanWorking.java index 2a6fce41c656d0..a9e5aa857f1d3a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsPodmanWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsPodmanWorking.java @@ -5,6 +5,7 @@ import java.util.List; import io.quarkus.deployment.util.ContainerRuntimeUtil; +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; public class IsPodmanWorking extends IsContainerRuntimeWorking { public IsPodmanWorking() { @@ -21,7 +22,11 @@ public IsPodmanWorking(boolean silent) { private static class PodmanBinaryStrategy implements Strategy { @Override public Result get() { - return (ContainerRuntimeUtil.detectContainerRuntime(false) != UNAVAILABLE) ? Result.AVAILABLE : Result.UNKNOWN; + if (ContainerRuntimeUtil.detectContainerRuntime(false, ContainerRuntime.PODMAN) != UNAVAILABLE) { + return Result.AVAILABLE; + } else { + return Result.UNKNOWN; + } } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java index 70cd34ba7fed40..9145a080dc9685 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java @@ -18,6 +18,9 @@ public class JniProcessor { JniConfig jni; + /** + * JNI + */ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) static class JniConfig { /** diff --git a/core/deployment/src/main/java/io/quarkus/deployment/PlatformConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/PlatformConfig.java index 82434cae34f93e..1c018ef9baca78 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/PlatformConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/PlatformConfig.java @@ -4,6 +4,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** + * Platform + *

* This is used currently only to suppress warnings about unknown properties * when the user supplies something like: -Dquarkus.platform.group-id=someGroup * diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java index c5380c77d56644..b423d833787567 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java @@ -1,6 +1,5 @@ package io.quarkus.deployment; -import java.io.Closeable; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -12,9 +11,11 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Supplier; import org.jboss.logging.Logger; +import io.quarkus.bootstrap.app.DependencyInfoProvider; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.builder.BuildChain; @@ -54,8 +55,10 @@ public class QuarkusAugmentor { private final Collection excludedFromIndexing; private final LiveReloadBuildItem liveReloadBuildItem; private final Properties buildSystemProperties; + private final Properties runtimeProperties; private final Path targetDir; private final ApplicationModel effectiveModel; + private final Supplier depInfoProvider; private final String baseName; private final String originalBaseName; private final boolean rebuild; @@ -73,6 +76,7 @@ public class QuarkusAugmentor { this.excludedFromIndexing = builder.excludedFromIndexing; this.liveReloadBuildItem = builder.liveReloadState; this.buildSystemProperties = builder.buildSystemProperties; + this.runtimeProperties = builder.runtimeProperties; this.targetDir = builder.targetDir; this.effectiveModel = builder.effectiveModel; this.baseName = builder.baseName; @@ -83,6 +87,7 @@ public class QuarkusAugmentor { this.auxiliaryApplication = builder.auxiliaryApplication; this.auxiliaryDevModeType = Optional.ofNullable(builder.auxiliaryDevModeType); this.test = builder.test; + this.depInfoProvider = builder.depInfoProvider; } public BuildResult run() throws Exception { @@ -99,13 +104,9 @@ public BuildResult run() throws Exception { final BuildChainBuilder chainBuilder = BuildChain.builder(); chainBuilder.setClassLoader(deploymentClassLoader); - //provideCapabilities(chainBuilder); - - //TODO: we load everything from the deployment class loader - //this allows the deployment config (application.properties) to be loaded, but in theory could result - //in additional stuff from the deployment leaking in, this is unlikely but has a bit of a smell. ExtensionLoader.loadStepsFrom(deploymentClassLoader, buildSystemProperties == null ? new Properties() : buildSystemProperties, + runtimeProperties == null ? new Properties() : runtimeProperties, effectiveModel, launchMode, devModeType) .accept(chainBuilder); @@ -153,7 +154,7 @@ public BuildResult run() throws Exception { auxiliaryDevModeType, test)) .produce(new BuildSystemTargetBuildItem(targetDir, baseName, originalBaseName, rebuild, buildSystemProperties == null ? new Properties() : buildSystemProperties)) - .produce(new AppModelProviderBuildItem(effectiveModel)); + .produce(new AppModelProviderBuildItem(effectiveModel, depInfoProvider)); for (PathCollection i : additionalApplicationArchives) { execBuilder.produce(new AdditionalApplicationArchiveBuildItem(i)); } @@ -181,9 +182,6 @@ public BuildResult run() throws Exception { .releaseConfig(deploymentClassLoader); } catch (Exception ignore) { - } - if (deploymentClassLoader instanceof Closeable) { - ((Closeable) deploymentClassLoader).close(); } Thread.currentThread().setContextClassLoader(originalClassLoader); buildCloseables.close(); @@ -210,6 +208,7 @@ public static final class Builder { LaunchMode launchMode = LaunchMode.NORMAL; LiveReloadBuildItem liveReloadState = new LiveReloadBuildItem(); Properties buildSystemProperties; + Properties runtimeProperties; ApplicationModel effectiveModel; String baseName = QUARKUS_APPLICATION; @@ -218,6 +217,7 @@ public static final class Builder { DevModeType devModeType; boolean test; boolean auxiliaryApplication; + private Supplier depInfoProvider; public Builder addBuildChainCustomizer(Consumer customizer) { this.buildChainCustomizers.add(customizer); @@ -321,6 +321,15 @@ public Builder setBuildSystemProperties(final Properties buildSystemProperties) return this; } + public Properties getRuntimeProperties() { + return runtimeProperties; + } + + public Builder setRuntimeProperties(final Properties runtimeProperties) { + this.runtimeProperties = runtimeProperties; + return this; + } + public Builder setRebuild(boolean rebuild) { this.rebuild = rebuild; return this; @@ -357,5 +366,10 @@ public Builder setDeploymentClassLoader(ClassLoader deploymentClassLoader) { this.deploymentClassLoader = deploymentClassLoader; return this; } + + public Builder setDependencyInfoProvider(Supplier depInfoProvider) { + this.depInfoProvider = depInfoProvider; + return this; + } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/SecureRandomProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/SecureRandomProcessor.java new file mode 100644 index 00000000000000..6ecec735827d8b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/SecureRandomProcessor.java @@ -0,0 +1,18 @@ +package io.quarkus.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; + +public class SecureRandomProcessor { + + @BuildStep + void registerReflectiveMethods(BuildProducer reflectiveMethods) { + // Called reflectively through java.security.SecureRandom.SecureRandom() + reflectiveMethods.produce(new ReflectiveMethodBuildItem( + getClass().getName(), + "sun.security.provider.NativePRNG", "", + java.security.SecureRandomParameters.class)); + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/SnapStartConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/SnapStartConfig.java index 540e9973f63b41..bfe08594a15bc3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/SnapStartConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/SnapStartConfig.java @@ -7,6 +7,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** + * SnapStart + *

* Configure the various optimization to use * SnapStart */ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/SslProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/SslProcessor.java index 4430394cbfda6a..627bcaadfeb29e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/SslProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/SslProcessor.java @@ -16,6 +16,9 @@ public class SslProcessor { SslConfig ssl; + /** + * SSL + */ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) static class SslConfig { /** diff --git a/core/deployment/src/main/java/io/quarkus/deployment/annotations/BuildProducer.java b/core/deployment/src/main/java/io/quarkus/deployment/annotations/BuildProducer.java index 5f59db467b6a37..2bec80a3b38fde 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/annotations/BuildProducer.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/annotations/BuildProducer.java @@ -1,5 +1,7 @@ package io.quarkus.deployment.annotations; +import java.util.Collection; + import io.quarkus.builder.item.BuildItem; /** @@ -16,4 +18,7 @@ public interface BuildProducer { void produce(T item); + default void produce(Collection items) { + items.forEach(this::produce); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java index 19e8d697a8ef40..aff617365d12eb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/AppModelProviderBuildItem.java @@ -1,7 +1,11 @@ package io.quarkus.deployment.builditem; +import java.util.Objects; +import java.util.function.Supplier; + import org.jboss.logging.Logger; +import io.quarkus.bootstrap.app.DependencyInfoProvider; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.PlatformImports; import io.quarkus.builder.item.SimpleBuildItem; @@ -12,9 +16,15 @@ public final class AppModelProviderBuildItem extends SimpleBuildItem { private static final Logger log = Logger.getLogger(AppModelProviderBuildItem.class); private final ApplicationModel appModel; + private final Supplier depInfoProvider; public AppModelProviderBuildItem(ApplicationModel appModel) { - this.appModel = appModel; + this(appModel, null); + } + + public AppModelProviderBuildItem(ApplicationModel appModel, Supplier depInfoProvider) { + this.appModel = Objects.requireNonNull(appModel); + this.depInfoProvider = depInfoProvider; } public ApplicationModel validateAndGet(BootstrapConfig config) { @@ -34,4 +44,8 @@ public ApplicationModel validateAndGet(BootstrapConfig config) { } return appModel; } + + public Supplier getDependencyInfoProvider() { + return depInfoProvider; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationArchivesBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationArchivesBuildItem.java index 154b6b1def97f8..2429ab00e0bca3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationArchivesBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationArchivesBuildItem.java @@ -15,11 +15,16 @@ public final class ApplicationArchivesBuildItem extends SimpleBuildItem { private final ApplicationArchive root; private final Collection applicationArchives; - private Set allArchives; + private final Set allArchives; public ApplicationArchivesBuildItem(ApplicationArchive root, Collection applicationArchives) { this.root = root; this.applicationArchives = applicationArchives; + + HashSet ret = new HashSet<>(applicationArchives); + ret.add(root); + + this.allArchives = Collections.unmodifiableSet(ret); } /** @@ -43,11 +48,6 @@ public Collection getApplicationArchives() { * @return A set of all application archives, including the root archive */ public Set getAllApplicationArchives() { - if (allArchives == null) { - HashSet ret = new HashSet<>(applicationArchives); - ret.add(root); - allArchives = Collections.unmodifiableSet(ret); - } return allArchives; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java index ec968f494b8b09..ea79ca227b8bf7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java @@ -8,6 +8,15 @@ import io.quarkus.builder.item.MultiBuildItem; +/** + * Transform a class using ASM {@link ClassVisitor}. Note that the transformation is performed after assembling the + * index and thus the changes won't be visible to any processor steps relying on the index. + *

+ * You may consider using {@code io.quarkus.arc.deployment.AnnotationsTransformerBuildItem} if your transformation + * should be visible for Arc. See also + * I Need To + * Transform Annotation Metadata section of Quarkus CDI integration guide. + */ public final class BytecodeTransformerBuildItem extends MultiBuildItem { final String classToTransform; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesResultBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesResultBuildItem.java index 7645cc96b49da3..5c59ad4e2ca26c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesResultBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesResultBuildItem.java @@ -20,11 +20,17 @@ public final class DevServicesResultBuildItem extends MultiBuildItem { private final String name; + private final String description; private final String containerId; private final Map config; public DevServicesResultBuildItem(String name, String containerId, Map config) { + this(name, null, containerId, config); + } + + public DevServicesResultBuildItem(String name, String description, String containerId, Map config) { this.name = name; + this.description = description; this.containerId = containerId; this.config = config; } @@ -33,6 +39,10 @@ public String getName() { return name; } + public String getDescription() { + return description; + } + public String getContainerId() { return containerId; } @@ -44,6 +54,7 @@ public Map getConfig() { public static class RunningDevService implements Closeable { private final String name; + private final String description; private final String containerId; private final Map config; private final Closeable closeable; @@ -54,12 +65,25 @@ private static Map mapOf(String key, String value) { return map; } - public RunningDevService(String name, String containerId, Closeable closeable, String key, String value) { - this(name, containerId, closeable, mapOf(key, value)); + public RunningDevService(String name, String containerId, Closeable closeable, String key, + String value) { + this(name, null, containerId, closeable, mapOf(key, value)); + } + + public RunningDevService(String name, String description, String containerId, Closeable closeable, String key, + String value) { + this(name, description, containerId, closeable, mapOf(key, value)); + } + + public RunningDevService(String name, String containerId, Closeable closeable, + Map config) { + this(name, null, containerId, closeable, config); } - public RunningDevService(String name, String containerId, Closeable closeable, Map config) { + public RunningDevService(String name, String description, String containerId, Closeable closeable, + Map config) { this.name = name; + this.description = description; this.containerId = containerId; this.closeable = closeable; this.config = Collections.unmodifiableMap(config); @@ -69,6 +93,10 @@ public String getName() { return name; } + public String getDescription() { + return description; + } + public String getContainerId() { return containerId; } @@ -93,7 +121,7 @@ public void close() throws IOException { } public DevServicesResultBuildItem toBuildItem() { - return new DevServicesResultBuildItem(name, containerId, config); + return new DevServicesResultBuildItem(name, description, containerId, config); } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/GeneratedResourceBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/GeneratedResourceBuildItem.java index 8cdfd9bd8526bd..75721b1f08ea71 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/GeneratedResourceBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/GeneratedResourceBuildItem.java @@ -6,9 +6,17 @@ public final class GeneratedResourceBuildItem extends MultiBuildItem { final String name; final byte[] data; - // This option is only meant to be set by extensions that also generated the resource on the file system - // and must rely on Quarkus not getting in the way of loading that resource. - // It is currently used by Kogito to get serving of static resources in Dev Mode by Vert.x + /** + * This option is only meant to be set by extensions that also generated the resource on the file system + * and must rely on Quarkus not getting in the way of loading that resource. + * It is currently used by Kogito to get serving of static resources in Dev Mode by Vert.x + *
+ * + * @deprecated If you want to serve static resources use + * {@link io.quarkus.vertx.http.deployment.spi.GeneratedStaticResourceBuildItem} + * instead. + */ + @Deprecated final boolean excludeFromDevCL; public GeneratedResourceBuildItem(String name, byte[] data) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAgentConfigDirectoryBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAgentConfigDirectoryBuildItem.java new file mode 100644 index 00000000000000..c85bd992bccb8a --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAgentConfigDirectoryBuildItem.java @@ -0,0 +1,25 @@ +package io.quarkus.deployment.builditem.nativeimage; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; + +/** + * Native configuration generated by native image agent can be integrated + * directly into subsequence native build steps, + * if the user enables {@link NativeConfig#agentConfigurationApply()}. + * This build item is used to transfer the native configuration folder path + * onto the {@link io.quarkus.deployment.pkg.steps.NativeImageBuildStep}. + * If the build item is passed, + * the directory is added to the native image build execution. + */ +public final class NativeImageAgentConfigDirectoryBuildItem extends SimpleBuildItem { + private final String directory; + + public NativeImageAgentConfigDirectoryBuildItem(String directory) { + this.directory = directory; + } + + public String getDirectory() { + return directory; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java index f58313c940ae5f..5e491ed3da0e3b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java @@ -2,15 +2,21 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.util.ArtifactResourceResolver; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathFilter; +import io.quarkus.util.GlobUtil; /** * A build item that indicates that a static resource should be included in the native image. *

* A static resource is a file that is not processed by the build steps, but is included in the native image as-is. - * The resource path passed to the constructor is a {@code /}-separated path name (with the same semantics as the parameters + * The resource path passed to the constructor is a {@code /}-separated path name (with the same semantics as the parameters) * passed to {@link java.lang.ClassLoader#getResources(String)}. *

* Related build items: @@ -23,6 +29,23 @@ public final class NativeImageResourceBuildItem extends MultiBuildItem { private final List resources; + /** + * Builds a {@code NativeImageResourceBuildItem} for the given artifact and path + * + * @param dependencies the resolved dependencies of the build + * @param artifactCoordinates the coordinates of the artifact containing the resources + * @param resourceFilter the filter for the resources in glob syntax (see {@link GlobUtil}) + * @return + */ + public static NativeImageResourceBuildItem ofDependencyResources( + Collection dependencies, + ArtifactCoords artifactCoordinates, + PathFilter resourceFilter) { + + var resolver = ArtifactResourceResolver.of(dependencies, artifactCoordinates); + return new NativeImageResourceBuildItem(resolver.resourceList(resourceFilter)); + } + public NativeImageResourceBuildItem(String... resources) { this.resources = Arrays.asList(resources); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveClassBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveClassBuildItem.java index d71a00b8e0eabe..ca4b0e37c7fdf0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveClassBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveClassBuildItem.java @@ -2,24 +2,29 @@ import static java.util.Arrays.stream; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.logging.Log; /** * Used to register a class for reflection in native mode */ public final class ReflectiveClassBuildItem extends MultiBuildItem { + // The names of the classes that should be registered for reflection private final List className; private final boolean methods; + private final boolean queryMethods; private final boolean fields; + private final boolean classes; private final boolean constructors; + private final boolean queryConstructors; private final boolean weak; private final boolean serialization; private final boolean unsafeAllocated; + private final String reason; public static Builder builder(Class... classes) { String[] classNames = stream(classes) @@ -38,25 +43,11 @@ public static Builder builder(String... classNames) { return new Builder().className(classNames); } - private ReflectiveClassBuildItem(boolean constructors, boolean methods, boolean fields, boolean weak, boolean serialization, - boolean unsafeAllocated, Class... classes) { - List names = new ArrayList<>(); - for (Class i : classes) { - if (i == null) { - throw new NullPointerException(); - } - names.add(i.getName()); - } - this.className = names; - this.methods = methods; - this.fields = fields; - this.constructors = constructors; - this.weak = weak; - this.serialization = serialization; - this.unsafeAllocated = unsafeAllocated; - if (weak && serialization) { - throw new RuntimeException("Weak reflection not supported with serialization"); - } + private ReflectiveClassBuildItem(boolean constructors, boolean queryConstructors, boolean methods, boolean queryMethods, + boolean fields, boolean getClasses, boolean weak, boolean serialization, boolean unsafeAllocated, String reason, + Class... classes) { + this(constructors, queryConstructors, methods, queryMethods, fields, getClasses, weak, serialization, + unsafeAllocated, reason, stream(classes).map(Class::getName).toArray(String[]::new)); } /** @@ -74,7 +65,7 @@ public ReflectiveClassBuildItem(boolean methods, boolean fields, Class... cla */ @Deprecated(since = "3.0", forRemoval = true) public ReflectiveClassBuildItem(boolean constructors, boolean methods, boolean fields, Class... classes) { - this(constructors, methods, fields, false, false, false, classes); + this(constructors, false, methods, false, fields, false, false, false, false, null, classes); } /** @@ -92,7 +83,7 @@ public ReflectiveClassBuildItem(boolean methods, boolean fields, String... class */ @Deprecated(since = "3.0", forRemoval = true) public ReflectiveClassBuildItem(boolean constructors, boolean methods, boolean fields, String... classNames) { - this(constructors, methods, fields, false, false, false, classNames); + this(constructors, false, methods, false, fields, false, false, false, classNames); } /** @@ -102,7 +93,7 @@ public ReflectiveClassBuildItem(boolean constructors, boolean methods, boolean f @Deprecated(since = "3.0", forRemoval = true) public ReflectiveClassBuildItem(boolean constructors, boolean methods, boolean fields, boolean serialization, String... classNames) { - this(constructors, methods, fields, false, serialization, false, classNames); + this(constructors, false, methods, false, fields, false, serialization, false, classNames); } public static ReflectiveClassBuildItem weakClass(String... classNames) { @@ -123,8 +114,17 @@ public static ReflectiveClassBuildItem serializationClass(String... classNames) return ReflectiveClassBuildItem.builder(classNames).serialization().build(); } - ReflectiveClassBuildItem(boolean constructors, boolean methods, boolean fields, boolean weak, boolean serialization, + @Deprecated(since = "3.14", forRemoval = true) + ReflectiveClassBuildItem(boolean constructors, boolean queryConstructors, boolean methods, boolean queryMethods, + boolean fields, boolean weak, boolean serialization, boolean unsafeAllocated, String... className) { + this(constructors, queryConstructors, methods, queryMethods, fields, false, weak, serialization, unsafeAllocated, + null, className); + } + + ReflectiveClassBuildItem(boolean constructors, boolean queryConstructors, boolean methods, boolean queryMethods, + boolean fields, boolean classes, boolean weak, boolean serialization, + boolean unsafeAllocated, String reason, String... className) { for (String i : className) { if (i == null) { throw new NullPointerException(); @@ -132,11 +132,29 @@ public static ReflectiveClassBuildItem serializationClass(String... classNames) } this.className = Arrays.asList(className); this.methods = methods; + if (methods && queryMethods) { + Log.warnf( + "Both methods and queryMethods are set to true for classes: %s. queryMethods is redundant and will be ignored", + String.join(", ", className)); + this.queryMethods = false; + } else { + this.queryMethods = queryMethods; + } this.fields = fields; + this.classes = classes; this.constructors = constructors; + if (constructors && queryConstructors) { + Log.warnf( + "Both constructors and queryConstructors are set to true for classes: %s. queryConstructors is redundant and will be ignored", + String.join(", ", className)); + this.queryConstructors = false; + } else { + this.queryConstructors = queryConstructors; + } this.weak = weak; this.serialization = serialization; this.unsafeAllocated = unsafeAllocated; + this.reason = reason; } public List getClassNames() { @@ -147,14 +165,26 @@ public boolean isMethods() { return methods; } + public boolean isQueryMethods() { + return queryMethods; + } + public boolean isFields() { return fields; } + public boolean isClasses() { + return classes; + } + public boolean isConstructors() { return constructors; } + public boolean isQueryConstructors() { + return queryConstructors; + } + /** * @deprecated As of GraalVM 21.2 finalFieldsWritable is no longer needed when registering fields for reflection. This will * be removed in a future verion of Quarkus. @@ -176,14 +206,22 @@ public boolean isUnsafeAllocated() { return unsafeAllocated; } + public String getReason() { + return reason; + } + public static class Builder { private String[] className; private boolean constructors = true; + private boolean queryConstructors; private boolean methods; + private boolean queryMethods; private boolean fields; + private boolean classes; private boolean weak; private boolean serialization; private boolean unsafeAllocated; + private String reason; private Builder() { } @@ -193,6 +231,10 @@ public Builder className(String[] className) { return this; } + /** + * Configures whether constructors should be registered for reflection (true by default). + * Setting this enables getting all declared constructors for the class as well as invoking them reflectively. + */ public Builder constructors(boolean constructors) { this.constructors = constructors; return this; @@ -202,6 +244,23 @@ public Builder constructors() { return constructors(true); } + /** + * Configures whether constructors should be registered for reflection, for query purposes only. + * Setting this enables getting all declared constructors for the class but does not allow invoking them reflectively. + */ + public Builder queryConstructors(boolean queryConstructors) { + this.queryConstructors = queryConstructors; + return this; + } + + public Builder queryConstructors() { + return queryConstructors(true); + } + + /** + * Configures whether methods should be registered for reflection. + * Setting this enables getting all declared methods for the class as well as invoking them reflectively. + */ public Builder methods(boolean methods) { this.methods = methods; return this; @@ -211,6 +270,24 @@ public Builder methods() { return methods(true); } + /** + * Configures whether declared methods should be registered for reflection, for query purposes only, + * i.e. {@link Class#getDeclaredMethods()}. Setting this enables getting all declared methods for the class but + * does not allow invoking them reflectively. + */ + public Builder queryMethods(boolean queryMethods) { + this.queryMethods = queryMethods; + return this; + } + + public Builder queryMethods() { + return queryMethods(true); + } + + /** + * Configures whether fields should be registered for reflection. + * Setting this enables getting all declared fields for the class as well as accessing them reflectively. + */ public Builder fields(boolean fields) { this.fields = fields; return this; @@ -220,6 +297,19 @@ public Builder fields() { return fields(true); } + /** + * Configures whether declared classes should be registered for reflection. + * Setting this enables getting all declared classes through Class.getClasses(). + */ + public Builder classes(boolean classes) { + this.classes = classes; + return this; + } + + public Builder classes() { + return fields(true); + } + /** * @deprecated As of GraalVM 21.2 finalFieldsWritable is no longer needed when registering fields for reflection. This * will be removed in a future version of Quarkus. @@ -238,6 +328,9 @@ public Builder weak() { return weak(true); } + /** + * Configures whether serialization support should be enabled for the class. + */ public Builder serialization(boolean serialization) { this.serialization = serialization; return this; @@ -247,17 +340,26 @@ public Builder serialization() { return serialization(true); } + /** + * Configures whether the class can be allocated in an unsafe manner (through JNI). + */ public Builder unsafeAllocated(boolean unsafeAllocated) { this.unsafeAllocated = unsafeAllocated; return this; } + public Builder reason(String reason) { + this.reason = reason; + return this; + } + public Builder unsafeAllocated() { return unsafeAllocated(true); } public ReflectiveClassBuildItem build() { - return new ReflectiveClassBuildItem(constructors, methods, fields, weak, serialization, unsafeAllocated, className); + return new ReflectiveClassBuildItem(constructors, queryConstructors, methods, queryMethods, fields, classes, weak, + serialization, unsafeAllocated, reason, className); } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveFieldBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveFieldBuildItem.java index 9738b2256b474f..ecfdfd7e491b31 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveFieldBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveFieldBuildItem.java @@ -10,15 +10,28 @@ public final class ReflectiveFieldBuildItem extends MultiBuildItem { final String declaringClass; final String name; + final String reason; + + public ReflectiveFieldBuildItem(String reason, FieldInfo field) { + this(reason, field.declaringClass().name().toString(), field.name()); + } public ReflectiveFieldBuildItem(FieldInfo field) { - this.name = field.name(); - this.declaringClass = field.declaringClass().name().toString(); + this(null, field); } public ReflectiveFieldBuildItem(Field field) { - this.name = field.getName(); - this.declaringClass = field.getDeclaringClass().getName(); + this(null, field); + } + + public ReflectiveFieldBuildItem(String reason, Field field) { + this(reason, field.getDeclaringClass().getName(), field.getName()); + } + + public ReflectiveFieldBuildItem(String reason, String declaringClass, String fieldName) { + this.reason = reason; + this.name = fieldName; + this.declaringClass = declaringClass; } public String getDeclaringClass() { @@ -28,4 +41,8 @@ public String getDeclaringClass() { public String getName() { return name; } + + public String getReason() { + return reason; + } } 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 bd13ee0931508e..54b7c35d01ffeb 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 @@ -192,7 +192,7 @@ public static Builder builder(DotName className) { * @return a new {@link Builder} instance, initialized from the specified {@link Type} */ public static Builder builder(Type type) { - return new Builder().type(type); + return new Builder(type); } public static class Builder { @@ -205,6 +205,23 @@ public static class Builder { private String source = UNKNOWN_SOURCE; private boolean serialization; + /** + * @deprecated use {@link ReflectiveHierarchyBuildItem#builder(Type)}, + * {@link ReflectiveHierarchyBuildItem#builder(String)} or + * {@link ReflectiveHierarchyBuildItem#builder(DotName)} instead + */ + @Deprecated(since = "3.12", forRemoval = true) + public Builder() { + } + + private Builder(Type type) { + this.type = type; + } + + /** + * @deprecated use {@link ReflectiveHierarchyBuildItem#builder(Type)} instead + */ + @Deprecated(since = "3.12", forRemoval = true) public Builder type(Type type) { this.type = type; return this; @@ -215,7 +232,9 @@ public Builder type(Type type) { * * @param className a {@link DotName} representing the name of the class of the Type to be registered for reflection * @return this {@link Builder} instance + * @deprecated use {@link ReflectiveHierarchyBuildItem#builder(DotName)} instead */ + @Deprecated(since = "3.12", forRemoval = true) public Builder className(DotName className) { return type(Type.create(className, Type.Kind.CLASS)); } @@ -225,7 +244,9 @@ public Builder className(DotName className) { * * @param className the name of the class of the Type to be registered for reflection * @return this {@link Builder} instance + * @deprecated use {@link ReflectiveHierarchyBuildItem#builder(String)} instead */ + @Deprecated(since = "3.12", forRemoval = true) public Builder className(String className) { return className(DotName.createSimple(className)); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveMethodBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveMethodBuildItem.java index 2a1f7515548db7..48692063f53441 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveMethodBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveMethodBuildItem.java @@ -13,44 +13,76 @@ public final class ReflectiveMethodBuildItem extends MultiBuildItem { final String declaringClass; final String name; final String[] params; + final boolean queryOnly; + final String reason; public ReflectiveMethodBuildItem(MethodInfo methodInfo) { - String[] params = new String[methodInfo.parametersCount()]; - for (int i = 0; i < params.length; ++i) { - params[i] = methodInfo.parameterType(i).name().toString(); - } - this.name = methodInfo.name(); - this.params = params; - this.declaringClass = methodInfo.declaringClass().name().toString(); + this(null, false, methodInfo); + } + + public ReflectiveMethodBuildItem(String reason, MethodInfo methodInfo) { + this(reason, false, methodInfo); + } + + public ReflectiveMethodBuildItem(boolean queryOnly, MethodInfo methodInfo) { + this(null, queryOnly, methodInfo); + } + + public ReflectiveMethodBuildItem(String reason, boolean queryOnly, MethodInfo methodInfo) { + this(reason, queryOnly, methodInfo.declaringClass().name().toString(), methodInfo.name(), + methodInfo.parameterTypes().stream().map(p -> p.name().toString()).toArray(String[]::new)); } public ReflectiveMethodBuildItem(Method method) { - this.params = new String[method.getParameterCount()]; - if (method.getParameterCount() > 0) { - Class[] parameterTypes = method.getParameterTypes(); - for (int i = 0; i < params.length; ++i) { - params[i] = parameterTypes[i].getName(); - } - } - this.name = method.getName(); - this.declaringClass = method.getDeclaringClass().getName(); + this(false, method); + } + + public ReflectiveMethodBuildItem(boolean queryOnly, Method method) { + this(null, queryOnly, method); + } + + public ReflectiveMethodBuildItem(String reason, boolean queryOnly, Method method) { + this(reason, queryOnly, method.getDeclaringClass().getName(), method.getName(), + Arrays.stream(method.getParameterTypes()).map(Class::getName).toArray(String[]::new)); } public ReflectiveMethodBuildItem(String declaringClass, String name, String... params) { + this(null, false, declaringClass, name, params); + } + + public ReflectiveMethodBuildItem(String reason, String declaringClass, String name, + String... params) { + this(reason, false, declaringClass, name, params); + } + + public ReflectiveMethodBuildItem(boolean queryOnly, String declaringClass, String name, + String... params) { + this(null, queryOnly, declaringClass, name, params); + } + + public ReflectiveMethodBuildItem(String reason, boolean queryOnly, String declaringClass, String name, + String... params) { this.declaringClass = declaringClass; this.name = name; this.params = params; + this.queryOnly = queryOnly; + this.reason = reason; + } + + public ReflectiveMethodBuildItem(String reason, String declaringClass, String name, + Class... params) { + this(reason, false, declaringClass, name, Arrays.stream(params).map(Class::getName).toArray(String[]::new)); } public ReflectiveMethodBuildItem(String declaringClass, String name, Class... params) { - this.declaringClass = declaringClass; - this.name = name; - this.params = new String[params.length]; - for (int i = 0; i < params.length; ++i) { - this.params[i] = params[i].getName(); - } + this(false, declaringClass, name, params); + } + + public ReflectiveMethodBuildItem(boolean queryOnly, String declaringClass, String name, + Class... params) { + this(null, queryOnly, declaringClass, name, Arrays.stream(params).map(Class::getName).toArray(String[]::new)); } public String getName() { @@ -65,6 +97,14 @@ public String getDeclaringClass() { return declaringClass; } + public boolean isQueryOnly() { + return queryOnly; + } + + public String getReason() { + return reason; + } + @Override public boolean equals(Object o) { if (this == o) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ServiceProviderBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ServiceProviderBuildItem.java index 28854dcb5bfc11..590adea75ab3f0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ServiceProviderBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ServiceProviderBuildItem.java @@ -11,7 +11,11 @@ import java.util.Set; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.util.ArtifactResourceResolver; import io.quarkus.deployment.util.ServiceUtil; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathFilter; /** * Represents a Service Provider registration. @@ -21,6 +25,8 @@ public final class ServiceProviderBuildItem extends MultiBuildItem { public static final String SPI_ROOT = "META-INF/services/"; + private static final PathFilter SPI_FILTER = PathFilter.forIncludes(List.of(SPI_ROOT + "*")); + private final String serviceInterface; private final List providers; @@ -52,7 +58,7 @@ public static ServiceProviderBuildItem allProviders(final String serviceInterfac line = line.substring(0, commentIndex); } line = line.trim(); - if (line.length() != 0) { + if (!line.isEmpty()) { classNames.add(line); } } @@ -87,6 +93,48 @@ public static ServiceProviderBuildItem allProvidersFromClassPath(final String se } } + /** + * Creates a new {@link Collection} of {@code ServiceProviderBuildItem}s for the selected artifact. + * It includes all the providers, that are contained in all the service interface descriptor files defined in + * {@code "META-INF/services/"} in the selected artifact. + * + * @param dependencies the resolved dependencies of the build + * @param artifactCoordinates the coordinates of the artifact containing the service definitions + * @return a {@link Collection} of {@code ServiceProviderBuildItem}s containing all the found service providers + */ + public static Collection allProvidersOfDependency( + Collection dependencies, + ArtifactCoords artifactCoordinates) { + + return allProvidersOfDependencies(dependencies, List.of(artifactCoordinates)); + } + + /** + * Creates a new {@link Collection} of {@code ServiceProviderBuildItem}s for the selected artifacts. + * It includes all the providers, that are contained in all the service interface descriptor files defined in + * {@code "META-INF/services/"} in all the selected artifacts. + * + * @param dependencies the resolved dependencies of the build + * @param artifactCoordinatesCollection a {@link Collection} of coordinates of the artifacts containing the service + * definitions + * @return a {@link Collection} of {@code ServiceProviderBuildItem}s containing all the found service providers + */ + public static Collection allProvidersOfDependencies( + Collection dependencies, + Collection artifactCoordinatesCollection) { + + var resolver = ArtifactResourceResolver.of(dependencies, artifactCoordinatesCollection); + return resolver.resourcePathList(SPI_FILTER).stream() + .map(ServiceProviderBuildItem::ofSpiPath) + .toList(); + } + + private static ServiceProviderBuildItem ofSpiPath(Path spiPath) { + return new ServiceProviderBuildItem( + spiPath.getFileName().toString(), + ServiceUtil.classNamesNamedIn(spiPath.toString())); + } + /** * Registers the specified service interface descriptor to be embedded and allow reflection (instantiation only) * of the specified provider classes. Note that the service interface descriptor file has to exist and match the @@ -136,12 +184,12 @@ private ServiceProviderBuildItem(String serviceInterfaceClassName, List this.providers = providers; // Validation - if (serviceInterface.length() == 0) { + if (serviceInterface.isEmpty()) { throw new IllegalArgumentException("The serviceDescriptorFile interface cannot be blank"); } providers.forEach(s -> { - if (s == null || s.length() == 0) { + if (s == null || s.isEmpty()) { throw new IllegalArgumentException("The provider class name cannot be null or blank"); } }); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java index 5fcf7264f97e2d..aeb85e44b89d6e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java @@ -6,6 +6,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Deployment + */ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class DeployConfig { /** diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index 36458eba693084..e493211f6336e1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -6,13 +6,11 @@ import static io.quarkus.deployment.util.ReflectUtil.toError; import static io.quarkus.deployment.util.ReflectUtil.typeOfParameter; import static io.quarkus.deployment.util.ReflectUtil.unwrapInvocationTargetException; -import static io.quarkus.runtime.configuration.PropertiesUtil.filterPropertiesInRoots; +import static io.quarkus.runtime.configuration.PropertiesUtil.isPropertyInRoots; import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; import static io.smallrye.config.Expressions.withoutExpansion; -import static io.smallrye.config.PropertiesConfigSourceProvider.classPathSources; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE_PARENT; -import static io.smallrye.config.SmallRyeConfigBuilder.META_INF_MICROPROFILE_CONFIG_PROPERTIES; import static java.util.stream.Collectors.toSet; import java.io.IOException; @@ -79,9 +77,8 @@ import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.ConfigValue; import io.smallrye.config.Converters; +import io.smallrye.config.DefaultValuesConfigSource; import io.smallrye.config.EnvConfigSource; -import io.smallrye.config.KeyMap; -import io.smallrye.config.KeyMapBackedConfigSource; import io.smallrye.config.ProfileConfigSourceInterceptor; import io.smallrye.config.PropertiesConfigSource; import io.smallrye.config.SecretKeys; @@ -89,7 +86,6 @@ import io.smallrye.config.SmallRyeConfigBuilder; import io.smallrye.config.SysPropConfigSource; import io.smallrye.config.common.AbstractConfigSource; -import io.smallrye.config.common.MapBackedConfigSource; /** * A configuration reader. @@ -384,29 +380,25 @@ public List getBuildTimeVisibleMappings() { * @param platformProperties Quarkus platform properties to add as a configuration source * @return configuration instance */ - public SmallRyeConfig initConfiguration(LaunchMode launchMode, Properties buildSystemProps, + public SmallRyeConfig initConfiguration(LaunchMode launchMode, Properties buildSystemProps, Properties runtimeProperties, Map platformProperties) { // now prepare & load the build configuration - final SmallRyeConfigBuilder builder = ConfigUtils.configBuilder(false, launchMode); + SmallRyeConfigBuilder builder = ConfigUtils.configBuilder(false, launchMode); if (classLoader != null) { builder.forClassLoader(classLoader); } - final DefaultValuesConfigurationSource ds1 = new DefaultValuesConfigurationSource(getBuildTimePatternMap()); - final DefaultValuesConfigurationSource ds2 = new DefaultValuesConfigurationSource(getBuildTimeRunTimePatternMap()); - final PropertiesConfigSource pcs = new PropertiesConfigSource(buildSystemProps, "Build system"); - if (platformProperties.isEmpty()) { - builder.withSources(ds1, ds2, pcs); - } else { - final KeyMap props = new KeyMap<>(platformProperties.size()); - for (Map.Entry prop : platformProperties.entrySet()) { - props.findOrAdd(new io.smallrye.config.NameIterator(prop.getKey())).putRootValue(prop.getValue()); - } - final KeyMapBackedConfigSource platformConfigSource = new KeyMapBackedConfigSource("Quarkus platform", - // Our default value configuration source is using an ordinal of Integer.MIN_VALUE - // (see io.quarkus.deployment.configuration.DefaultValuesConfigurationSource) - Integer.MIN_VALUE + 1000, props); - builder.withSources(ds1, ds2, platformConfigSource, pcs); + builder + .withSources(new DefaultValuesConfigurationSource(getBuildTimePatternMap())) + .withSources(new DefaultValuesConfigurationSource(getBuildTimeRunTimePatternMap())) + .withSources(new PropertiesConfigSource(buildSystemProps, "Build system")) + .withSources(new PropertiesConfigSource(runtimeProperties, "Runtime Properties")); + + if (!platformProperties.isEmpty()) { + // Our default value configuration source is using an ordinal of Integer.MIN_VALUE + // (see io.quarkus.deployment.configuration.DefaultValuesConfigurationSource) + builder.withSources( + new DefaultValuesConfigSource(platformProperties, "Quarkus platform", Integer.MIN_VALUE + 1000)); } for (ConfigClassWithPrefix mapping : getBuildTimeVisibleMappings()) { @@ -537,7 +529,7 @@ ReadResult run() { } NameIterator ni = new NameIterator(propertyName); - if (ni.hasNext() && PropertiesUtil.isPropertyInRoot(registeredRoots, ni)) { + if (ni.hasNext() && PropertiesUtil.isPropertyInRoots(propertyName, registeredRoots)) { // build time patterns Container matched = buildTimePatternMap.match(ni); boolean knownProperty = matched != null; @@ -616,13 +608,7 @@ ReadResult run() { // it's not managed by us; record it ConfigValue configValue = withoutExpansion(() -> runtimeConfig.getConfigValue(propertyName)); if (configValue.getValue() != null) { - String configName = configValue.getNameProfiled(); - // record the profile parent in the original form; if recorded in the active profile it may mess the profile ordering - if (configName.equals("quarkus.config.profile.parent")) { - runTimeValues.put(propertyName, configValue.getValue()); - } else { - runTimeValues.put(configName, configValue.getValue()); - } + runTimeValues.put(propertyName, configValue.getValue()); } // in the case the user defined compound keys in YAML (or similar config source, that quotes the name) @@ -1058,7 +1044,7 @@ private Set getAllProperties(final Set registeredRoots) { unprofiledProperty = property.substring(profileDot + 1); } } - if (filterPropertiesInRoots(List.of(unprofiledProperty), registeredRoots).iterator().hasNext()) { + if (PropertiesUtil.isPropertyInRoots(unprofiledProperty, registeredRoots)) { sourcesProperties.add(property); } } @@ -1095,42 +1081,25 @@ public String getValue(final String propertyName) { properties.add(property); } - // TODO - Add better API to set an empty Profile, or no Profile at all // We also need an empty profile Config to record the properties that are not on the active profile builder = ConfigUtils.emptyConfigBuilder(); + // Do not use a profile, so we can record both profile properties and main properties of the active profile + builder.getProfiles().add(""); builder.getSources().clear(); builder.getSourceProviders().clear(); builder.setAddDefaultSources(false) .withInterceptors(ConfigCompatibility.FrontEnd.nonLoggingInstance(), ConfigCompatibility.BackEnd.instance()) .addDiscoveredCustomizers() - .withSources(sourceProperties) - .withSources(new MapBackedConfigSource( - "Reset Profile", - Map.of("quarkus.profile", "", - "quarkus.config.profile.parent", "", - "quarkus.test.profile", "", - SMALLRYE_CONFIG_PROFILE, "", - SMALLRYE_CONFIG_PROFILE_PARENT, "", - Config.PROFILE, ""), - Integer.MAX_VALUE) { - @Override - public Set getPropertyNames() { - return Collections.emptySet(); - } - }); + .withSources(sourceProperties); List profiles = config.getProfiles(); for (String property : builder.build().getPropertyNames()) { String activeProperty = ProfileConfigSourceInterceptor.activeName(property, profiles); // keep the profile parent in the original form; if we use the active profile it may mess the profile ordering - if (activeProperty.equals("quarkus.config.profile.parent")) { - if (!activeProperty.equals(property)) { - properties.remove(activeProperty); - properties.add(property); - continue; - } + if (activeProperty.equals("quarkus.config.profile.parent") && !activeProperty.equals(property)) { + properties.remove(activeProperty); } - properties.add(activeProperty); + properties.add(property); } return properties; @@ -1147,13 +1116,14 @@ public Set getPropertyNames() { */ private SmallRyeConfig getConfigForRuntimeRecording() { SmallRyeConfigBuilder builder = ConfigUtils.emptyConfigBuilder(); + // Do not use a profile, so we can record both profile properties and main properties of the active profile + builder.getProfiles().add(""); builder.getSources().clear(); builder.getSourceProviders().clear(); builder.setAddDefaultSources(false) // Customizers may duplicate sources, but not much we can do about it, we need to run them .addDiscoveredCustomizers() - // Read microprofile-config.properties, because we disabled the default sources - .withSources(classPathSources(META_INF_MICROPROFILE_CONFIG_PROPERTIES, classLoader)); + .addPropertiesSources(); // TODO - Should we reset quarkus.config.location to not record from these sources? for (ConfigSource configSource : config.getConfigSources()) { @@ -1188,7 +1158,7 @@ public String getValue(final String propertyName) { return config.getConfigValue(propertyName).getValue(); } return null; - }; + } }); return builder.build(); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java index 9f6c050e6bf055..3eb7b693f46ecb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java @@ -11,6 +11,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** + * Class loading + *

* WARNING: This is not normal quarkus config, this is only read from application.properties. *

* This is because it is needed before any of the config infrastructure is set up. diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java index 8710a0211c4cb8..07cf447e6c9180 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java @@ -61,12 +61,6 @@ public final class ConfigCompatibility { ConfigCompatibility::quarkusPackageDecompilerEnabled), entry(List.of("quarkus", "package", "decompiler", "jar-directory"), ConfigCompatibility::quarkusPackageDecompilerJarDirectory), - entry(List.of("quarkus", "package", "vineflower", "version"), - ConfigCompatibility::quarkusPackageDecompilerVersion), - entry(List.of("quarkus", "package", "vineflower", "enabled"), - ConfigCompatibility::quarkusPackageDecompilerEnabled), - entry(List.of("quarkus", "package", "vineflower", "jar-directory"), - ConfigCompatibility::quarkusPackageDecompilerJarDirectory), entry(List.of("quarkus", "package", "manifest", "attributes", "*"), ConfigCompatibility::quarkusPackageManifestAttributes), entry(List.of("quarkus", "package", "manifest", "sections", "*", "*"), @@ -285,12 +279,12 @@ private static List quarkusPackageDecompilerVersion(ConfigSourceIntercep private static List quarkusPackageDecompilerEnabled(ConfigSourceInterceptorContext ctxt, NameIterator ni) { // simple mapping to a new name - return List.of("quarkus.package.decompiler.enabled"); + return List.of("quarkus.package.jar.decompiler.enabled"); } private static List quarkusPackageDecompilerJarDirectory(ConfigSourceInterceptorContext ctxt, NameIterator ni) { // simple mapping to a new name - return List.of("quarkus.package.decompiler.jar-directory"); + return List.of("quarkus.package.jar.decompiler.jar-directory"); } private static List quarkusPackageManifestAttributes(ConfigSourceInterceptorContext ctxt, NameIterator ni) { @@ -510,11 +504,7 @@ private static ConfigValue quarkusPackageJarManifestAddImplementationEntries(Con private static ConfigValue quarkusPackageJarDecompilerEnabled(ConfigSourceInterceptorContext ctxt, NameIterator ni) { ConfigValue oldVal = ctxt.restart("quarkus.package.decompiler.enabled"); if (oldVal == null) { - oldVal = ctxt.restart("quarkus.package.vineflower.enabled"); - if (oldVal == null) { - // on to the default value - return ctxt.proceed(ni.getName()); - } + return ctxt.proceed(ni.getName()); } // map old name to new name return oldVal.withName(ni.getName()); @@ -523,11 +513,7 @@ private static ConfigValue quarkusPackageJarDecompilerEnabled(ConfigSourceInterc private static ConfigValue quarkusPackageJarDecompilerJarDirectory(ConfigSourceInterceptorContext ctxt, NameIterator ni) { ConfigValue oldVal = ctxt.restart("quarkus.package.decompiler.jar-directory"); if (oldVal == null) { - oldVal = ctxt.restart("quarkus.package.vineflower.jar-directory"); - if (oldVal == null) { - // on to the default value - return ctxt.proceed(ni.getName()); - } + return ctxt.proceed(ni.getName()); } // map old name to new name return oldVal.withName(ni.getName()); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java index 293ef9e3b3dbc9..546572ed3ef283 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java @@ -119,10 +119,13 @@ private static void processConfigClass( classBytes)); additionalConstrainedClasses.produce(AdditionalConstrainedClassBuildItem.of(mappingMetadata.getClassName(), classBytes)); - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()).constructors().build()); - reflectiveMethods - .produce(new ReflectiveMethodBuildItem(mappingMetadata.getClassName(), "getDefaults", new String[0])); - reflectiveMethods.produce(new ReflectiveMethodBuildItem(mappingMetadata.getClassName(), "getNames", new String[0])); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()) + .reason(ConfigMappingUtils.class.getName()) + .build()); + reflectiveMethods.produce(new ReflectiveMethodBuildItem(ConfigMappingUtils.class.getName(), + mappingMetadata.getClassName(), "getDefaults", new String[0])); + reflectiveMethods.produce(new ReflectiveMethodBuildItem(ConfigMappingUtils.class.getName(), + mappingMetadata.getClassName(), "getNames", new String[0])); configComponentInterfaces.add(mappingMetadata.getInterfaceType()); @@ -141,6 +144,8 @@ private static void processProperties( ConfigMappingInterface mapping = ConfigMappingLoader.getConfigMapping(configClass); for (Property property : mapping.getProperties()) { Class returnType = property.getMethod().getReturnType(); + String reason = ConfigMappingUtils.class.getSimpleName() + " Required to process property " + + property.getPropertyName(); if (property.hasConvertWith()) { Class> convertWith; @@ -149,39 +154,44 @@ private static void processProperties( } else { convertWith = property.asPrimitive().getConvertWith(); } - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(convertWith).build()); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(convertWith).reason(reason).build()); } - registerImplicitConverter(property, reflectiveClasses); + registerImplicitConverter(property, reason, reflectiveClasses); if (property.isMap()) { MapProperty mapProperty = property.asMap(); if (mapProperty.hasKeyConvertWith()) { - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyConvertWith()).build()); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyConvertWith()).reason(reason).build()); } else { - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyRawType()).build()); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyRawType()).reason(reason).build()); } - registerImplicitConverter(mapProperty.getValueProperty(), reflectiveClasses); + registerImplicitConverter(mapProperty.getValueProperty(), reason, reflectiveClasses); } } } private static void registerImplicitConverter( Property property, - BuildProducer reflectiveClasses) { + String reason, BuildProducer reflectiveClasses) { if (property.isLeaf() && !property.isOptional()) { LeafProperty leafProperty = property.asLeaf(); if (leafProperty.hasConvertWith()) { - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(leafProperty.getConvertWith()).build()); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(leafProperty.getConvertWith()).reason(reason).build()); } else { - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(leafProperty.getValueRawType()).methods().build()); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(leafProperty.getValueRawType()).reason(reason).methods() + .build()); } } else if (property.isOptional()) { - registerImplicitConverter(property.asOptional().getNestedProperty(), reflectiveClasses); + registerImplicitConverter(property.asOptional().getNestedProperty(), reason, reflectiveClasses); } else if (property.isCollection()) { - registerImplicitConverter(property.asCollection().getElement(), reflectiveClasses); + registerImplicitConverter(property.asCollection().getElement(), reason, reflectiveClasses); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingConfig.java index 6508e33023b89c..225b27d3db5876 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingConfig.java @@ -13,6 +13,8 @@ import io.smallrye.config.WithDefault; /** + * Configuration tracking and dumping + *

* Configuration options for application build time configuration usage tracking * and dumping. */ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingInterceptor.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingInterceptor.java index 3e5f92866492ad..0b341494da8b34 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingInterceptor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingInterceptor.java @@ -62,7 +62,7 @@ public Map getReadOptions() { * @param config configuration instance */ public void configure(Config config) { - enabled = config.getValue("quarkus.config-tracking.enabled", boolean.class); + enabled = config.getOptionalValue("quarkus.config-tracking.enabled", boolean.class).orElse(false); if (enabled) { readOptions = new ConcurrentHashMap<>(); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleConfig.java index 433c9129d4f8da..e5b683ebd966d2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleConfig.java @@ -3,6 +3,9 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Console + */ @ConfigRoot public class ConsoleConfig { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassComparisonUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassComparisonUtil.java index a7f18fb788e720..8c87e8f7249161 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassComparisonUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassComparisonUtil.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -19,6 +20,10 @@ import org.jboss.jandex.Type; public class ClassComparisonUtil { + private static final Set IGNORED_ANNOTATIONS = Set.of( + DotName.createSimple("kotlin.jvm.internal.SourceDebugExtension"), + DotName.createSimple("kotlin.Metadata")); + static boolean isSameStructure(ClassInfo clazz, ClassInfo old) { if (clazz.flags() != old.flags()) { return false; @@ -161,6 +166,9 @@ private static void methodMap(Collection b, List valuesA = a.values(); List valuesB = b.values(); if (valuesA.size() != valuesB.size()) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java index 02bfdcd3405e51..6377b9c3b908a5 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java @@ -40,7 +40,7 @@ public class DevModeMain implements Closeable { private final DevModeContext context; - private static volatile CuratedApplication curatedApplication; + private volatile CuratedApplication curatedApplication; private Closeable realCloseable; public DevModeMain(DevModeContext context) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java index 0fbd24bd9f3b7b..890a586828c5e1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java @@ -15,6 +15,7 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -29,14 +30,12 @@ import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.RunningQuarkusApplication; import io.quarkus.bootstrap.app.StartupAction; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.logging.InitialConfigurator; import io.quarkus.bootstrap.runner.Timing; import io.quarkus.builder.BuildChainBuilder; import io.quarkus.builder.BuildContext; import io.quarkus.builder.BuildStep; -import io.quarkus.commons.classloading.ClassloadHelper; import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; import io.quarkus.deployment.console.ConsoleCommand; import io.quarkus.deployment.console.ConsoleStateManager; @@ -61,16 +60,16 @@ public class IsolatedDevModeMain implements BiConsumer hotReplacementSetups = new ArrayList<>(); - private static volatile RunningQuarkusApplication runner; - static volatile Throwable deploymentProblem; - private static volatile CuratedApplication curatedApplication; - private static volatile AugmentAction augmentAction; - private static volatile boolean restarting; - private static volatile boolean firstStartCompleted; - private static final CountDownLatch shutdownLatch = new CountDownLatch(1); + private volatile RunningQuarkusApplication runner; + final AtomicReference deploymentProblem = new AtomicReference<>(); + private volatile CuratedApplication curatedApplication; + private volatile AugmentAction augmentAction; + private volatile boolean restarting; + private volatile boolean firstStartCompleted; + private final CountDownLatch shutdownLatch = new CountDownLatch(1); private Thread shutdownThread; private CodeGenWatcher codeGenWatcher; - private static volatile ConsoleStateManager.ConsoleContext consoleContext; + private volatile ConsoleStateManager.ConsoleContext consoleContext; private final List listeners = new ArrayList<>(); private synchronized void firstStart() { @@ -83,38 +82,18 @@ private synchronized void firstStart() { //this is a bit yuck, but we need replace the default //exit handler in the runtime class loader //TODO: look at implementing a common core classloader, that removes the need for this sort of crappy hack - curatedApplication.getBaseRuntimeClassLoader().loadClass(ApplicationLifecycleManager.class.getName()) + curatedApplication.getOrCreateBaseRuntimeClassLoader().loadClass(ApplicationLifecycleManager.class.getName()) .getMethod("setDefaultExitCodeHandler", Consumer.class) - .invoke(null, new Consumer() { - @Override - public void accept(Integer integer) { - if (restarting || ApplicationLifecycleManager.isVmShuttingDown() - || context.isAbortOnFailedStart() || - context.isTest()) { - return; - } - if (consoleContext == null) { - consoleContext = ConsoleStateManager.INSTANCE - .createContext("Completed Application"); - } - //this sucks, but when we get here logging is gone - //so we just setup basic console logging - InitialConfigurator.DELAYED_HANDLER.addHandler(new ConsoleHandler( - ConsoleHandler.Target.SYSTEM_OUT, - new ColorPatternFormatter("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n"))); - consoleContext.reset(new ConsoleCommand(' ', "Restarts the application", "to restart", 0, null, - () -> { - consoleContext.reset(); - RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); - })); - } - }); + .invoke(null, getExitCodeHandler()); StartupAction start = augmentAction.createInitialRuntimeApplication(); runner = start.runMainClass(context.getArgs()); - RuntimeUpdatesProcessor.INSTANCE.setConfiguredInstrumentationEnabled( - runner.getConfigValue("quarkus.live-reload.instrumentation", Boolean.class).orElse(false)); + RuntimeUpdatesProcessor.INSTANCE + .setConfiguredInstrumentationEnabled( + runner.getConfigValue("quarkus.live-reload.instrumentation", Boolean.class).orElse(false)) + .setLiveReloadEnabled( + runner.getConfigValue("quarkus.live-reload.enabled", Boolean.class).orElse(false)); firstStartCompleted = true; notifyListenersAfterStart(); @@ -124,7 +103,7 @@ public void accept(Integer integer) { rootCause = rootCause.getCause(); } if (!(rootCause instanceof BindException)) { - deploymentProblem = t; + deploymentProblem.set(t); if (!context.isAbortOnFailedStart()) { //we need to set this here, while we still have the correct TCCL //this is so the config is still valid, and we can read HTTP config from application.properties @@ -136,7 +115,8 @@ public void accept(Integer integer) { ApplicationStateNotification.notifyStartupFailed(t); if (RuntimeUpdatesProcessor.INSTANCE != null) { - Thread.currentThread().setContextClassLoader(curatedApplication.getBaseRuntimeClassLoader()); + Thread.currentThread() + .setContextClassLoader(curatedApplication.getOrCreateBaseRuntimeClassLoader()); try { if (!InitialConfigurator.DELAYED_HANDLER.isActivated()) { Class cl = Thread.currentThread().getContextClassLoader() @@ -170,6 +150,35 @@ public void accept(Integer integer) { } } + private Consumer getExitCodeHandler() { + if (context.isTest() || context.isAbortOnFailedStart()) { + return TestExitCodeHandler.INSTANCE; + } + + return new Consumer() { + @Override + public void accept(Integer integer) { + if (restarting || ApplicationLifecycleManager.isVmShuttingDown()) { + return; + } + if (consoleContext == null) { + consoleContext = ConsoleStateManager.INSTANCE + .createContext("Completed Application"); + } + //this sucks, but when we get here logging is gone + //so we just setup basic console logging + InitialConfigurator.DELAYED_HANDLER.addHandler(new ConsoleHandler( + ConsoleHandler.Target.SYSTEM_OUT, + new ColorPatternFormatter("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n"))); + consoleContext.reset(new ConsoleCommand(' ', "Restarts the application", "to restart", 0, null, + () -> { + consoleContext.reset(); + RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); + })); + } + }; + } + public void restartCallback(Set changedResources, ClassScanResult result) { restartApp(changedResources, new ClassChangeInformation(result.changedClassNames, result.deletedClassNames, result.addedClassNames)); @@ -181,8 +190,8 @@ public synchronized void restartApp(Set changedResources, ClassChangeInf consoleContext.reset(); } stop(); - Timing.restart(curatedApplication.getAugmentClassLoader()); - deploymentProblem = null; + Timing.restart(curatedApplication.getOrCreateAugmentClassLoader()); + deploymentProblem.set(null); ClassLoader old = Thread.currentThread().getContextClassLoader(); try { @@ -196,14 +205,14 @@ public synchronized void restartApp(Set changedResources, ClassChangeInf firstStartCompleted = true; } } catch (Throwable t) { - deploymentProblem = t; + deploymentProblem.set(t); Throwable rootCause = t; while (rootCause.getCause() != null) { rootCause = rootCause.getCause(); } if (!(rootCause instanceof BindException)) { log.error("Failed to start quarkus", t); - Thread.currentThread().setContextClassLoader(curatedApplication.getAugmentClassLoader()); + Thread.currentThread().setContextClassLoader(curatedApplication.getOrCreateAugmentClassLoader()); LoggingSetupRecorder.handleFailedStart(); } } @@ -249,22 +258,22 @@ private RuntimeUpdatesProcessor setupRuntimeCompilation(DevModeContext context, public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }, testSupport); + }, testSupport, deploymentProblem); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, - curatedApplication.getBaseRuntimeClassLoader())) { + curatedApplication.getOrCreateBaseRuntimeClassLoader())) { hotReplacementSetups.add(service); service.setupHotDeployment(processor); processor.addHotReplacementSetup(service); } for (DeploymentFailedStartHandler service : ServiceLoader.load(DeploymentFailedStartHandler.class, - curatedApplication.getAugmentClassLoader())) { + curatedApplication.getOrCreateAugmentClassLoader())) { processor.addDeploymentFailedStartHandler(new Runnable() { @Override public void run() { ClassLoader old = Thread.currentThread().getContextClassLoader(); try { - Thread.currentThread().setContextClassLoader(curatedApplication.getAugmentClassLoader()); + Thread.currentThread().setContextClassLoader(curatedApplication.getOrCreateAugmentClassLoader()); service.handleFailedInitialStart(); } finally { Thread.currentThread().setContextClassLoader(old); @@ -307,6 +316,7 @@ public void close() { restarting = true; if (codeGenWatcher != null) { codeGenWatcher.shutdown(); + codeGenWatcher = null; } for (int i = listeners.size() - 1; i >= 0; i--) { @@ -316,6 +326,7 @@ public void close() { log.warn("Unable to invoke 'beforeShutdown' of " + listeners.get(i).getClass(), e); } } + listeners.clear(); try { stop(); @@ -337,10 +348,14 @@ public void close() { for (HotReplacementSetup i : hotReplacementSetups) { i.close(); } + hotReplacementSetups.clear(); } finally { try { DevConsoleManager.close(); curatedApplication.close(); + curatedApplication = null; + augmentAction = null; + deploymentProblem.set(null); } finally { if (shutdownThread != null) { try { @@ -362,7 +377,7 @@ public void accept(CuratedApplication o, Map params) { //setup the dev mode thread pool for NIO System.setProperty("java.nio.channels.DefaultThreadPool.threadFactory", "io.quarkus.dev.io.NioThreadPoolThreadFactory"); - Timing.staticInitStarted(o.getBaseRuntimeClassLoader(), false); + Timing.staticInitStarted(o.getOrCreateBaseRuntimeClassLoader(), false); //https://github.com/quarkusio/quarkus/issues/9748 //if you have an app with all daemon threads then the app thread //may be the only thread keeping the JVM alive @@ -397,29 +412,7 @@ public void run() { } augmentAction = new AugmentActionImpl(curatedApplication, - List.of(new Consumer() { - @Override - public void accept(BuildChainBuilder buildChainBuilder) { - buildChainBuilder.addBuildStep(new BuildStep() { - @Override - public void execute(BuildContext context) { - //we need to make sure all hot reloadable classes are application classes - context.produce(new ApplicationClassPredicateBuildItem(new Predicate() { - @Override - public boolean test(String s) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - String resourceName = ClassloadHelper.fromClassNameToResourceName(s); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(resourceName, true); - return !res.isEmpty(); - } - })); - } - }).produces(ApplicationClassPredicateBuildItem.class).build(); - } - }), + List.of(new AddApplicationClassPredicateBuildStep()), List.of()); // code generators should be initialized before the runtime compilation is setup to properly configure the sources directories @@ -435,10 +428,11 @@ public boolean test(String s) { firstStart(); // doStart(false, Collections.emptySet()); - if (deploymentProblem != null || RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() != null) { + if (deploymentProblem.get() != null || RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() != null) { if (context.isAbortOnFailedStart()) { - Throwable throwable = deploymentProblem == null ? RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() - : deploymentProblem; + Throwable throwable = deploymentProblem.get() == null + ? RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() + : deploymentProblem.get(); throw (throwable instanceof RuntimeException ? (RuntimeException) throwable : new RuntimeException(throwable)); @@ -471,4 +465,33 @@ public void run() { throw toThrow; } } + + private static class AddApplicationClassPredicateBuildStep implements Consumer { + + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + //we need to make sure all hot reloadable classes are application classes + context.produce(new ApplicationClassPredicateBuildItem(new Predicate() { + @Override + public boolean test(String className) { + return QuarkusClassLoader.isApplicationClass(className); + } + })); + } + }).produces(ApplicationClassPredicateBuildItem.class).build(); + } + } + + private static class TestExitCodeHandler implements Consumer { + + private static final TestExitCodeHandler INSTANCE = new TestExitCodeHandler(); + + @Override + public void accept(Integer exitCode) { + // do nothing + } + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java index 1ec369cd140fbf..17d4b5e547fb81 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; @@ -57,7 +58,7 @@ public class IsolatedRemoteDevModeMain implements BiConsumer hotReplacementSetups = new ArrayList<>(); - static volatile Throwable deploymentProblem; + private AtomicReference deploymentProblem = new AtomicReference<>(); static volatile RemoteDevClient remoteDevClient; static volatile Closeable remoteDevClientSession; private static volatile CuratedApplication curatedApplication; @@ -68,7 +69,7 @@ public class IsolatedRemoteDevModeMain implements BiConsumer providers = ServiceLoader.load(RemoteDevClientProvider.class, - curatedApplication.getAugmentClassLoader()); + curatedApplication.getOrCreateAugmentClassLoader()); RemoteDevClient client = null; for (RemoteDevClientProvider provider : providers) { Optional opt = provider.getClient(); @@ -99,7 +100,7 @@ private synchronized JarResult generateApplication() { curatedApplication.getApplicationModel(), null); return start.getJar(); } catch (Throwable t) { - deploymentProblem = t; + deploymentProblem.set(t); log.error("Failed to generate Quarkus application", t); return null; } @@ -137,22 +138,22 @@ public void accept(DevModeContext.ModuleInfo moduleInfo, String s) { public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }, null); + }, null, deploymentProblem); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, - curatedApplication.getBaseRuntimeClassLoader())) { + curatedApplication.getOrCreateBaseRuntimeClassLoader())) { hotReplacementSetups.add(service); service.setupHotDeployment(processor); processor.addHotReplacementSetup(service); } for (DeploymentFailedStartHandler service : ServiceLoader.load(DeploymentFailedStartHandler.class, - curatedApplication.getAugmentClassLoader())) { + curatedApplication.getOrCreateAugmentClassLoader())) { processor.addDeploymentFailedStartHandler(new Runnable() { @Override public void run() { ClassLoader old = Thread.currentThread().getContextClassLoader(); try { - Thread.currentThread().setContextClassLoader(curatedApplication.getAugmentClassLoader()); + Thread.currentThread().setContextClassLoader(curatedApplication.getOrCreateAugmentClassLoader()); service.handleFailedInitialStart(); } finally { Thread.currentThread().setContextClassLoader(old); @@ -189,6 +190,7 @@ public void close() { } } } finally { + deploymentProblem.set(null); curatedApplication.close(); } @@ -198,7 +200,7 @@ public void close() { @Override public void accept(CuratedApplication o, Map o2) { LoggingSetupRecorder.handleFailedStart(); //we are not going to actually run an app - Timing.staticInitStarted(o.getBaseRuntimeClassLoader(), false); + Timing.staticInitStarted(o.getOrCreateBaseRuntimeClassLoader(), false); try { curatedApplication = o; Object potentialContext = o2.get(DevModeContext.class.getName()); @@ -248,7 +250,7 @@ public void run() { } private Closeable doConnect() { - return remoteDevClient.sendConnectRequest(new RemoteDevState(currentHashes, deploymentProblem), + return remoteDevClient.sendConnectRequest(new RemoteDevState(currentHashes, deploymentProblem.get()), new Function, Map>() { @Override public Map apply(Set fileNames) { @@ -283,6 +285,7 @@ private RemoteDevClient.SyncResult runSync() { Set removed = new HashSet<>(); Map changed = new HashMap<>(); try { + deploymentProblem.set(null); boolean scanResult = RuntimeUpdatesProcessor.INSTANCE.doScan(true); if (!scanResult && !copiedStaticResources.isEmpty()) { scanResult = true; @@ -305,7 +308,7 @@ private RemoteDevClient.SyncResult runSync() { currentHashes = newHashes; } } catch (IOException e) { - deploymentProblem = e; + deploymentProblem.set(e); } return new RemoteDevClient.SyncResult() { @Override diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java index e58029cd79cf26..9fa94f1ff6aecd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java @@ -36,7 +36,6 @@ public class IsolatedTestModeMain extends IsolatedDevModeMain { private volatile DevModeContext context; private final List hotReplacementSetups = new ArrayList<>(); - static volatile Throwable deploymentProblem; private static volatile CuratedApplication curatedApplication; private static volatile AugmentAction augmentAction; @@ -68,10 +67,10 @@ public void accept(DevModeContext.ModuleInfo moduleInfo, String s) { public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }, testSupport); + }, testSupport, deploymentProblem); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, - curatedApplication.getBaseRuntimeClassLoader())) { + curatedApplication.getOrCreateBaseRuntimeClassLoader())) { hotReplacementSetups.add(service); service.setupHotDeployment(processor); processor.addHotReplacementSetup(service); @@ -107,7 +106,7 @@ public void close() { public void accept(CuratedApplication o, Map params) { System.setProperty("java.nio.channels.DefaultThreadPool.threadFactory", "io.quarkus.dev.io.NioThreadPoolThreadFactory"); - Timing.staticInitStarted(o.getBaseRuntimeClassLoader(), false); + Timing.staticInitStarted(o.getOrCreateBaseRuntimeClassLoader(), false); try { curatedApplication = o; Object potentialContext = params.get(DevModeContext.class.getName()); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java index 580506a736255f..d5e2b05b68da44 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java @@ -85,12 +85,6 @@ public B debug(String debug) { return (B) this; } - @SuppressWarnings("unchecked") - public B debugPortOk(Boolean debugPortOk) { - QuarkusDevModeLauncher.this.debugPortOk = debugPortOk; - return (B) this; - } - @SuppressWarnings("unchecked") public B suspend(String suspend) { QuarkusDevModeLauncher.this.suspend = suspend; @@ -303,10 +297,10 @@ public R build() throws Exception { private List args = new ArrayList<>(0); private String debug; - private Boolean debugPortOk; private String suspend; private String debugHost = "localhost"; private String debugPort = "5005"; + private String actualDebugPort; private File projectDir; private File buildDir; private File outputDir; @@ -390,12 +384,13 @@ protected void prepare() throws Exception { if (debug != null && debug.equalsIgnoreCase("client")) { args.add("-agentlib:jdwp=transport=dt_socket,address=" + debugHost + ":" + port + ",server=n,suspend=" + suspend); + actualDebugPort = String.valueOf(port); } else if (debug == null || !debug.equalsIgnoreCase("false")) { // if the debug port is used, we want to make an effort to pick another one // if we can't find an open port, we don't fail the process launch, we just don't enable debugging // Furthermore, we don't check this on restarts, as the previous process is still running boolean warnAboutChange = false; - if (debugPortOk == null) { + if (actualDebugPort == null) { int tries = 0; while (true) { boolean isPortUsed; @@ -408,20 +403,19 @@ protected void prepare() throws Exception { isPortUsed = false; } if (!isPortUsed) { - debugPortOk = true; + actualDebugPort = String.valueOf(port); break; } if (++tries >= 5) { - debugPortOk = false; break; } else { port = getRandomPort(); } } } - if (debugPortOk) { + if (actualDebugPort != null) { if (warnAboutChange) { - warn("Changed debug port to " + port + " because of a port conflict"); + warn("Changed debug port to " + actualDebugPort + " because of a port conflict"); } args.add("-agentlib:jdwp=transport=dt_socket,address=" + debugHost + ":" + port + ",server=y,suspend=" + suspend); @@ -547,8 +541,8 @@ public List args() { return args; } - public Boolean getDebugPortOk() { - return debugPortOk; + public String getActualDebugPort() { + return actualDebugPort; } protected abstract boolean isDebugEnabled(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index 46b3ba3b1862c0..d69f80e7cf25fd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -40,6 +40,7 @@ import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; @@ -91,6 +92,7 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable volatile Throwable compileProblem; volatile Throwable testCompileProblem; volatile Throwable hotReloadProblem; + private final AtomicReference deploymentProblem; private volatile Predicate disableInstrumentationForClassPredicate = new AlwaysFalsePredicate<>(); private volatile Predicate disableInstrumentationForIndexPredicate = new AlwaysFalsePredicate<>(); @@ -141,7 +143,7 @@ public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, Qua DevModeType devModeType, BiConsumer, ClassScanResult> restartCallback, BiConsumer copyResourceNotification, BiFunction classTransformers, - TestSupport testSupport) { + TestSupport testSupport, AtomicReference deploymentProblem) { this.applicationRoot = applicationRoot; this.context = context; this.compiler = compiler; @@ -180,6 +182,7 @@ public void testsDisabled() { } }); } + this.deploymentProblem = deploymentProblem; } public TestSupport getTestSupport() { @@ -187,12 +190,13 @@ public TestSupport getTestSupport() { } @Override - public Path getClassesDir() { - //TODO: fix all these - for (DevModeContext.ModuleInfo i : context.getAllModules()) { - return Paths.get(i.getMain().getClassesPath()); + public List getClassesDir() { + final List allModules = context.getAllModules(); + final List paths = new ArrayList<>(allModules.size()); + for (DevModeContext.ModuleInfo i : allModules) { + paths.add(Path.of(i.getMain().getClassesPath())); } - return null; + return paths; } @Override @@ -245,13 +249,17 @@ public void handleChanges(Collection changes) { periodicTestCompile(); } }; + // monitor .env as it can impact test execution + testClassChangeWatcher.watchFiles(Path.of(context.getApplicationRoot().getProjectDirectory()), + List.of(Path.of(".env")), + callback); Set nonExistent = new HashSet<>(); for (DevModeContext.ModuleInfo module : context.getAllModules()) { for (Path path : module.getMain().getSourcePaths()) { - testClassChangeWatcher.watchPath(path.toFile(), callback); + testClassChangeWatcher.watchDirectoryRecursively(path, callback); } for (Path path : module.getMain().getResourcePaths()) { - testClassChangeWatcher.watchPath(path.toFile(), callback); + testClassChangeWatcher.watchDirectoryRecursively(path, callback); } } for (DevModeContext.ModuleInfo module : context.getAllModules()) { @@ -260,14 +268,14 @@ public void handleChanges(Collection changes) { if (!Files.isDirectory(path)) { nonExistent.add(path); } else { - testClassChangeWatcher.watchPath(path.toFile(), callback); + testClassChangeWatcher.watchDirectoryRecursively(path, callback); } } for (Path path : module.getTest().get().getResourcePaths()) { if (!Files.isDirectory(path)) { nonExistent.add(path); } else { - testClassChangeWatcher.watchPath(path.toFile(), callback); + testClassChangeWatcher.watchDirectoryRecursively(path, callback); } } } @@ -283,7 +291,7 @@ public void run() { Path i = iterator.next(); if (Files.isDirectory(i)) { iterator.remove(); - testClassChangeWatcher.watchPath(i.toFile(), callback); + testClassChangeWatcher.watchDirectoryRecursively(i, callback); added = true; } @@ -391,7 +399,7 @@ public List getResourcesDir() { public Throwable getDeploymentProblem() { //we differentiate between these internally, however for the error reporting they are the same return compileProblem != null ? compileProblem - : IsolatedDevModeMain.deploymentProblem != null ? IsolatedDevModeMain.deploymentProblem + : deploymentProblem.get() != null ? deploymentProblem.get() : hotReloadProblem; } @@ -534,7 +542,7 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) { //all broken we just assume the reason that they have refreshed is because they have fixed something //trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged() - || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || fileRestartNeeded); + || (deploymentProblem.get() != null && userInitiated) || fileRestartNeeded); if (restartNeeded) { String changeString = changedFilesForRestart.stream().map(Path::getFileName).map(Object::toString) .collect(Collectors.joining(", ")); @@ -618,6 +626,11 @@ public boolean instrumentationEnabled() { return configuredInstrumentationEnabled; } + public RuntimeUpdatesProcessor setLiveReloadEnabled(boolean liveReloadEnabled) { + this.liveReloadEnabled = liveReloadEnabled; + return this; + } + public RuntimeUpdatesProcessor setConfiguredInstrumentationEnabled(boolean configuredInstrumentationEnabled) { this.configuredInstrumentationEnabled = configuredInstrumentationEnabled; return this; @@ -1203,11 +1216,32 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map // Then process glob patterns for (Entry e : watchedFilePaths.entrySet()) { String watchedFilePath = e.getKey(); - Path path = Paths.get(watchedFilePath); - if (!path.isAbsolute() && !watchedRootPaths.contains(e.getKey()) && maybeGlobPattern(watchedFilePath)) { - Path resolvedPath = root.resolve(watchedFilePath); - for (WatchedPath extra : expandGlobPattern(root, resolvedPath, watchedFilePath, e.getValue())) { - timestamps.watchedPaths.put(extra.filePath, extra); + Path path = Paths.get(sanitizedPattern(watchedFilePath)); + if (!path.isAbsolute() && !watchedRootPaths.contains(e.getKey()) + && maybeGlobPattern(watchedFilePath)) { + try { + final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + watchedFilePath); + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + Path relativePath = root.relativize(file); + if (matcher.matches(relativePath)) { + log.debugf("Glob pattern [%s] matched %s from %s", watchedFilePath, relativePath, + root); + WatchedPath extra = new WatchedPath(file, relativePath, e.getValue(), + attrs.lastModifiedTime().toMillis()); + timestamps.watchedPaths.put(extra.filePath, extra); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); } } } @@ -1218,8 +1252,9 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map // Finally process watched absolute paths for (Entry e : watchedFilePaths.entrySet()) { String watchedFilePath = e.getKey(); - Path path = Paths.get(watchedFilePath); + Path path = Paths.get(sanitizedPattern(watchedFilePath)); if (path.isAbsolute()) { + path = Paths.get(watchedFilePath); log.debugf("Watch %s", path); if (Files.exists(path)) { putLastModifiedTime(path, path, e.getValue(), timestamps); @@ -1234,6 +1269,10 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map return this; } + private String sanitizedPattern(String pattern) { + return pattern.replaceAll("[*?]", ""); + } + private boolean maybeGlobPattern(String path) { return path.contains("*") || path.contains("?"); } @@ -1281,32 +1320,6 @@ public void close() throws IOException { } } - private List expandGlobPattern(Path root, Path path, String pattern, boolean restart) { - PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + path.toString()); - List files = new ArrayList<>(); - try { - Files.walkFileTree(root, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (pathMatcher.matches(file)) { - Path relativePath = root.relativize(file); - log.debugf("Glob pattern [%s] matched %s from %s", pattern, relativePath, root); - files.add(new WatchedPath(file, relativePath, restart, attrs.lastModifiedTime().toMillis())); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) { - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return files; - } - public boolean toggleInstrumentation() { instrumentationEnabled = !instrumentationEnabled(); if (instrumentationEnabled) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/DevServiceDescriptionBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/DevServiceDescriptionBuildItem.java index c5b7651297c0e5..e1898f49dee2c8 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/DevServiceDescriptionBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/DevServiceDescriptionBuildItem.java @@ -9,14 +9,22 @@ public final class DevServiceDescriptionBuildItem extends MultiBuildItem { private String name; + private String description; private ContainerInfo containerInfo; private Map configs; public DevServiceDescriptionBuildItem() { } - public DevServiceDescriptionBuildItem(String name, ContainerInfo containerInfo, Map configs) { + public DevServiceDescriptionBuildItem(String name, ContainerInfo containerInfo, + Map configs) { + this(name, null, containerInfo, configs); + } + + public DevServiceDescriptionBuildItem(String name, String description, ContainerInfo containerInfo, + Map configs) { this.name = name; + this.description = description; this.containerInfo = containerInfo; this.configs = configs instanceof SortedMap ? configs : new TreeMap<>(configs); } @@ -29,6 +37,10 @@ public String getName() { return name; } + public String getDescription() { + return description; + } + public ContainerInfo getContainerInfo() { return containerInfo; } @@ -41,6 +53,10 @@ public void setName(String name) { this.name = name; } + public void setDescription(String description) { + this.description = description; + } + public void setContainerInfo(ContainerInfo containerInfo) { this.containerInfo = containerInfo; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java index 12102e2d700954..147e4bf0295228 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java @@ -7,6 +7,9 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Dev Services + */ @ConfigRoot(name = "devservices") public class GlobalDevServicesConfig { @@ -17,7 +20,7 @@ public class GlobalDevServicesConfig { boolean enabled; /** - * Global flag that can be used to force the attachmment of Dev Services to shared netxork. Default is false. + * Global flag that can be used to force the attachmment of Dev Services to shared network. Default is false. */ @ConfigItem(defaultValue = "false") public boolean launchOnSharedNetwork; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/QuarkusFileManager.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/QuarkusFileManager.java index 4ac5314804078e..71bd65e54830db 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/QuarkusFileManager.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/QuarkusFileManager.java @@ -22,6 +22,8 @@ protected QuarkusFileManager(StandardJavaFileManager fileManager, Context contex this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, List.of(context.getGeneratedSourcesDirectory())); } if (context.getAnnotationProcessorPaths() != null) { + // Paths might be missing! (see: https://github.com/quarkusio/quarkus/issues/42908) + ensureDirectories(context.getAnnotationProcessorPaths()); this.fileManager.setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, context.getAnnotationProcessorPaths()); } } catch (IOException e) { @@ -39,6 +41,8 @@ public void reset(Context context) { this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, List.of(context.getGeneratedSourcesDirectory())); } if (context.getAnnotationProcessorPaths() != null) { + // Paths might be missing! (see: https://github.com/quarkusio/quarkus/issues/42908) + ensureDirectories(context.getAnnotationProcessorPaths()); this.fileManager.setLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH, context.getAnnotationProcessorPaths()); } } catch (IOException e) { @@ -46,6 +50,17 @@ public void reset(Context context) { } } + private void ensureDirectories(Iterable directories) { + for (File directory : directories) { + if (!directory.exists()) { + final boolean success = directory.mkdirs(); + if (!success) { + throw new RuntimeException("Cannot create directory " + directory); + } + } + } + } + @Override public void close() throws IOException { super.close(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/ReloadableFileManager.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/ReloadableFileManager.java index f3ae3a718453bf..f2b6043d06244c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/ReloadableFileManager.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/ReloadableFileManager.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.dev.filesystem; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.File; import java.io.IOException; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/StaticFileManager.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/StaticFileManager.java index d3706c1e133e54..acfad174712378 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/StaticFileManager.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/filesystem/StaticFileManager.java @@ -31,8 +31,9 @@ public Iterable getJavaSources(Iterable files = Collections.synchronizedMap(new HashMap()); + private final Map monitoredDirectories = Collections.synchronizedMap(new HashMap<>()); private final Map pathDataByKey = Collections - .synchronizedMap(new IdentityHashMap()); + .synchronizedMap(new IdentityHashMap<>()); private volatile boolean stopped = false; private final Thread watchThread; @@ -70,19 +72,19 @@ public void run() { try { PathData pathData = pathDataByKey.get(key); if (pathData != null) { - final List results = new ArrayList(); + final List results = new ArrayList<>(); List> events = key.pollEvents(); - final Set addedFiles = new HashSet(); - final Set deletedFiles = new HashSet(); + final Set addedFiles = new HashSet<>(); + final Set deletedFiles = new HashSet<>(); for (WatchEvent event : events) { Path eventPath = (Path) event.context(); - File targetFile = ((Path) key.watchable()).resolve(eventPath).toFile(); + Path targetFile = ((Path) key.watchable()).resolve(eventPath).toAbsolutePath(); FileChangeEvent.Type type; if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) { type = FileChangeEvent.Type.ADDED; addedFiles.add(targetFile); - if (targetFile.isDirectory()) { + if (Files.isDirectory(targetFile)) { try { addWatchedDirectory(pathData, targetFile); } catch (IOException e) { @@ -107,6 +109,12 @@ public void run() { Iterator it = results.iterator(); while (it.hasNext()) { FileChangeEvent event = it.next(); + + if (!pathData.isMonitored(event.getFile())) { + it.remove(); + continue; + } + if (event.getType() == FileChangeEvent.Type.MODIFIED) { if (addedFiles.contains(event.getFile()) && deletedFiles.contains(event.getFile())) { @@ -134,7 +142,7 @@ public void run() { } if (!results.isEmpty()) { - for (FileChangeCallback callback : pathData.callbacks) { + for (FileChangeCallback callback : pathData.getCallbacks()) { invokeCallback(callback, results); } } @@ -142,7 +150,7 @@ public void run() { } finally { //if the key is no longer valid remove it from the files list if (!key.reset()) { - files.remove(key.watchable()); + monitoredDirectories.remove(key.watchable()); } } } @@ -156,39 +164,59 @@ public void run() { } } - public synchronized void watchPath(File file, FileChangeCallback callback) { + public synchronized void watchDirectoryRecursively(Path directory, FileChangeCallback callback) { try { - PathData data = files.get(file); + Path absoluteDirectory = directory.toAbsolutePath(); + PathData data = monitoredDirectories.get(absoluteDirectory); if (data == null) { - Set allDirectories = doScan(file).keySet(); - Path path = Paths.get(file.toURI()); - data = new PathData(path); - for (File dir : allDirectories) { + Set allDirectories = doScan(absoluteDirectory).keySet(); + data = new PathData(absoluteDirectory, List.of()); + for (Path dir : allDirectories) { addWatchedDirectory(data, dir); } - files.put(file, data); + monitoredDirectories.put(absoluteDirectory, data); + } + data.addCallback(callback); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * @param directory a directory that will be watched + * @param monitoredFiles list of monitored files relative to directory. An empty list will monitor all files. + * @param callback callback called when a file is changed + */ + public synchronized void watchFiles(Path directory, List monitoredFiles, FileChangeCallback callback) { + try { + Path absoluteDirectory = directory.toAbsolutePath(); + PathData data = monitoredDirectories.get(absoluteDirectory); + if (data == null) { + data = new PathData(absoluteDirectory, monitoredFiles); + addWatchedDirectory(data, absoluteDirectory); + monitoredDirectories.put(absoluteDirectory, data); } - data.callbacks.add(callback); + data.addCallback(callback); } catch (IOException e) { throw new RuntimeException(e); } } - private void addWatchedDirectory(PathData data, File dir) throws IOException { - Path path = Paths.get(dir.toURI()); - WatchKey key = path.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + private void addWatchedDirectory(PathData data, Path dir) throws IOException { + WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); pathDataByKey.put(key, data); - data.keys.add(key); + data.addWatchKey(key); } - public synchronized void unwatchPath(File file, final FileChangeCallback callback) { - PathData data = files.get(file); + public synchronized void unwatchPath(Path directory, final FileChangeCallback callback) { + PathData data = monitoredDirectories.get(directory); if (data != null) { - data.callbacks.remove(callback); - if (data.callbacks.isEmpty()) { - files.remove(file); - for (WatchKey key : data.keys) { + data.removeCallback(callback); + if (data.getCallbacks().isEmpty()) { + monitoredDirectories.remove(directory); + for (WatchKey key : data.getWatchKeys()) { key.cancel(); pathDataByKey.remove(key); } @@ -205,20 +233,21 @@ public void close() throws IOException { } } - private static Map doScan(File file) { - final Map results = new HashMap(); + private static Map doScan(Path directory) { + final Map results = new HashMap<>(); - final Deque toScan = new ArrayDeque(); - toScan.add(file); + final Deque toScan = new ArrayDeque<>(); + toScan.add(directory); while (!toScan.isEmpty()) { - File next = toScan.pop(); - if (next.isDirectory()) { - results.put(next, next.lastModified()); - File[] list = next.listFiles(); - if (list != null) { - for (File f : list) { - toScan.push(new File(f.getAbsolutePath())); + Path next = toScan.pop(); + if (Files.isDirectory(next)) { + try { + results.put(next, Files.getLastModifiedTime(directory).toMillis()); + try (Stream list = Files.list(next)) { + list.forEach(p -> toScan.push(p.toAbsolutePath())); } + } catch (IOException e) { + throw new UncheckedIOException("Unable to scan: " + next, e); } } } @@ -234,12 +263,52 @@ private static void invokeCallback(FileChangeCallback callback, List callbacks = new ArrayList(); - final List keys = new ArrayList(); - private PathData(Path path) { + private final Path path; + private final List callbacks = new ArrayList<>(); + private final List watchKeys = new ArrayList<>(); + private final List monitoredFiles; + + private PathData(Path path, List monitoredFiles) { this.path = path; + this.monitoredFiles = monitoredFiles.stream().map(p -> path.resolve(p).toAbsolutePath()) + .collect(Collectors.toList()); + } + + private void addWatchKey(WatchKey key) { + this.watchKeys.add(key); + } + + private void addCallback(FileChangeCallback callback) { + this.callbacks.add(callback); + } + + private void removeCallback(FileChangeCallback callback) { + this.callbacks.remove(callback); + } + + private List getCallbacks() { + return callbacks; + } + + private List getWatchKeys() { + return watchKeys; + } + + private boolean isMonitored(Path file) { + if (monitoredFiles.isEmpty()) { + return true; + } + + Path absolutePath = file.isAbsolute() ? file : file.toAbsolutePath(); + + for (Path monitoredFile : monitoredFiles) { + if (monitoredFile.equals(absolutePath)) { + return true; + } + } + + return false; } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java index d1b59bbb95935a..6f0f4bce6ea974 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.dev.testing; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.IOException; import java.io.InputStream; @@ -46,6 +46,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.reporting.ReportEntry; @@ -258,8 +259,9 @@ public void executionSkipped(TestIdentifier testIdentifier, String reason) { if (testClass != null) { Map results = resultsByClass.computeIfAbsent(testClass.getName(), s -> new HashMap<>()); - TestResult result = new TestResult(displayName, testClass.getName(), id, - TestExecutionResult.aborted(null), + TestResult result = new TestResult(displayName, testClass.getName(), + toTagList(testIdentifier), + id, TestExecutionResult.aborted(null), logHandler.captureOutput(), testIdentifier.isTest(), runId, 0, true); results.put(id, result); if (result.isTest()) { @@ -312,8 +314,9 @@ public void executionFinished(TestIdentifier testIdentifier, } Map results = resultsByClass.computeIfAbsent(testClassName, s -> new HashMap<>()); - TestResult result = new TestResult(displayName, testClassName, id, - testExecutionResult, + TestResult result = new TestResult(displayName, testClassName, + toTagList(testIdentifier), + id, testExecutionResult, logHandler.captureOutput(), testIdentifier.isTest(), runId, System.currentTimeMillis() - startTimes.get(testIdentifier), true); if (!results.containsKey(id)) { @@ -332,6 +335,7 @@ public void executionFinished(TestIdentifier testIdentifier, results.put(id, new TestResult(currentNonDynamicTest.get().getDisplayName(), result.getTestClass(), + toTagList(testIdentifier), currentNonDynamicTest.get().getUniqueIdObject(), TestExecutionResult.failed(failure), List.of(), false, runId, 0, false)); @@ -349,6 +353,7 @@ public void executionFinished(TestIdentifier testIdentifier, for (TestIdentifier child : children) { UniqueId childId = UniqueId.parse(child.getUniqueId()); result = new TestResult(child.getDisplayName(), testClassName, + toTagList(testIdentifier), childId, testExecutionResult, logHandler.captureOutput(), child.isTest(), runId, @@ -419,6 +424,15 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e } } + private static List toTagList(TestIdentifier testIdentifier) { + return testIdentifier + .getTags() + .stream() + .map(TestTag::getName) + .sorted() + .toList(); + } + private Class getTestClassFromSource(Optional optionalTestSource) { if (optionalTestSource.isPresent()) { var testSource = optionalTestSource.get(); @@ -652,7 +666,7 @@ public String apply(Class aClass) { //this is a lot more complex //we need to transform the classes to make the tracing magic work QuarkusClassLoader deploymentClassLoader = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); - Set classesToTransform = new HashSet<>(deploymentClassLoader.getLocalClassNames()); + Set classesToTransform = new HashSet<>(deploymentClassLoader.getReloadableClassNames()); Map transformedClasses = new HashMap<>(); for (String i : classesToTransform) { try { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java index be0ca21445bf00..228665b40573f7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java @@ -40,7 +40,7 @@ public synchronized void abort() { Runnable prepare(ClassScanResult classScanResult, boolean reRunFailures, long runId, TestRunListener listener) { var old = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(testApplication.getAugmentClassLoader()); + Thread.currentThread().setContextClassLoader(testApplication.getOrCreateAugmentClassLoader()); try { synchronized (this) { if (runner != null) { @@ -84,7 +84,7 @@ public FilterResult apply(TestDescriptor testDescriptor) { @Override public void run() { var old = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(testApplication.getAugmentClassLoader()); + Thread.currentThread().setContextClassLoader(testApplication.getOrCreateAugmentClassLoader()); try { prepared.run(); } finally { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java index 5c82f54fe447d0..edb4f110f223bb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java @@ -11,6 +11,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** + * Testing + *

* This is used currently only to suppress warnings about unknown properties * when the user supplies something like: -Dquarkus.test.profile=someProfile or -Dquarkus.test.native-image-profile=someProfile *

@@ -226,6 +228,7 @@ public class TestConfig { * most parent first classes it will just cause problems. */ @ConfigItem(defaultValue = "java\\..*") + @Deprecated(forRemoval = true) String classClonePattern; /** diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java index 4250d332492dc1..4f5025391a0b3a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java @@ -11,6 +11,7 @@ public class TestResult { final String displayName; final String testClass; + final List tags; final UniqueId uniqueId; final TestExecutionResult testExecutionResult; final List logOutput; @@ -20,10 +21,12 @@ public class TestResult { final List problems; final boolean reportable; - public TestResult(String displayName, String testClass, UniqueId uniqueId, TestExecutionResult testExecutionResult, + public TestResult(String displayName, String testClass, List tags, UniqueId uniqueId, + TestExecutionResult testExecutionResult, List logOutput, boolean test, long runId, long time, boolean reportable) { this.displayName = displayName; this.testClass = testClass; + this.tags = tags; this.uniqueId = uniqueId; this.testExecutionResult = testExecutionResult; this.logOutput = logOutput; @@ -58,6 +61,10 @@ public String getTestClass() { return testClass; } + public List getTags() { + return tags; + } + public UniqueId getUniqueId() { return uniqueId; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java index 61bb2d278d1c26..b27ba0042b125a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java @@ -1,7 +1,6 @@ package io.quarkus.deployment.dev.testing; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; @@ -13,7 +12,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Properties; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; @@ -42,7 +40,8 @@ import io.quarkus.dev.testing.TestWatchedFiles; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.PathList; -import io.smallrye.config.Converters; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; public class TestSupport implements TestController { @@ -76,13 +75,13 @@ public class TestSupport implements TestController { private Throwable compileProblem; private volatile boolean firstRun = true; - String appPropertiesIncludeTags; - String appPropertiesExcludeTags; + List appPropertiesIncludeTags; + List appPropertiesExcludeTags; String appPropertiesIncludePattern; String appPropertiesExcludePattern; - String appPropertiesIncludeEngines; - String appPropertiesExcludeEngines; - String appPropertiesTestType; + List appPropertiesIncludeEngines; + List appPropertiesExcludeEngines; + TestType appPropertiesTestType; private TestConfig config; private volatile boolean closed; @@ -222,7 +221,7 @@ public void init() { + curatedApplication.getClassLoaderNameSuffix(), getClass().getClassLoader().getParent(), false); } - clBuilder.addElement(ClassPathElement.fromDependency(d)); + clBuilder.addNormalPriorityElement(ClassPathElement.fromDependency(d)); } } @@ -509,90 +508,68 @@ public void addListener(TestListener listener) { * We also can't apply this as part of the test startup, as it is too * late and the filters have already been resolved. *

- * We manually check for application.properties changes and apply them. + * We manually check for configuration changes and apply them. */ private void handleApplicationPropertiesChange() { - for (Path rootPath : curatedApplication.getQuarkusBootstrap().getApplicationRoot()) { - Path appProps = rootPath.resolve("application.properties"); - if (Files.exists(appProps)) { - Properties p = new Properties(); - try (InputStream in = Files.newInputStream(appProps)) { - p.load(in); - } catch (IOException e) { - throw new RuntimeException(e); + SmallRyeConfig updatedConfig = getMinimalConfig(); + + List includeTags = getTrimmedListFromConfig(updatedConfig, "quarkus.test.include-tags").orElse(null); + List excludeTags = getTrimmedListFromConfig(updatedConfig, "quarkus.test.exclude-tags").orElse(null); + String includePattern = updatedConfig.getOptionalValue("quarkus.test.include-pattern", String.class).orElse(null); + String excludePattern = updatedConfig.getOptionalValue("quarkus.test.exclude-pattern", String.class).orElse(null); + List includeEngines = getTrimmedListFromConfig(updatedConfig, "quarkus.test.include-engines").orElse(null); + List excludeEngines = getTrimmedListFromConfig(updatedConfig, "quarkus.test.exclude-engines").orElse(null); + TestType testType = updatedConfig.getOptionalValue("quarkus.test.type", TestType.class).orElse(TestType.ALL); + + if (!firstRun) { + if (!Objects.equals(includeTags, appPropertiesIncludeTags)) { + this.includeTags = Objects.requireNonNullElse(includeTags, Collections.emptyList()); + } + if (!Objects.equals(excludeTags, appPropertiesExcludeTags)) { + this.excludeTags = Objects.requireNonNullElse(excludeTags, Collections.emptyList()); + } + if (!Objects.equals(includePattern, appPropertiesIncludePattern)) { + if (includePattern == null) { + this.include = null; + } else { + this.include = Pattern.compile(includePattern); } - String includeTags = p.getProperty("quarkus.test.include-tags"); - String excludeTags = p.getProperty("quarkus.test.exclude-tags"); - String includePattern = p.getProperty("quarkus.test.include-pattern"); - String excludePattern = p.getProperty("quarkus.test.exclude-pattern"); - String includeEngines = p.getProperty("quarkus.test.include-engines"); - String excludeEngines = p.getProperty("quarkus.test.exclude-engines"); - String testType = p.getProperty("quarkus.test.type"); - if (!firstRun) { - if (!Objects.equals(includeTags, appPropertiesIncludeTags)) { - if (includeTags == null) { - this.includeTags = Collections.emptyList(); - } else { - this.includeTags = Arrays.stream(includeTags.split(",")).map(String::trim) - .collect(Collectors.toList()); - } - } - if (!Objects.equals(excludeTags, appPropertiesExcludeTags)) { - if (excludeTags == null) { - this.excludeTags = Collections.emptyList(); - } else { - this.excludeTags = Arrays.stream(excludeTags.split(",")).map(String::trim) - .collect(Collectors.toList()); - } - } - if (!Objects.equals(includePattern, appPropertiesIncludePattern)) { - if (includePattern == null) { - include = null; - } else { - include = Pattern.compile(includePattern); - } - } - if (!Objects.equals(excludePattern, appPropertiesExcludePattern)) { - if (excludePattern == null) { - exclude = null; - } else { - exclude = Pattern.compile(excludePattern); - } - } - if (!Objects.equals(includeEngines, appPropertiesIncludeEngines)) { - if (includeEngines == null) { - this.includeEngines = Collections.emptyList(); - } else { - this.includeEngines = Arrays.stream(includeEngines.split(",")).map(String::trim) - .collect(Collectors.toList()); - } - } - if (!Objects.equals(excludeEngines, appPropertiesExcludeEngines)) { - if (excludeEngines == null) { - this.excludeEngines = Collections.emptyList(); - } else { - this.excludeEngines = Arrays.stream(excludeEngines.split(",")).map(String::trim) - .collect(Collectors.toList()); - } - } - if (!Objects.equals(testType, appPropertiesTestType)) { - if (testType == null) { - this.testType = TestType.ALL; - } else { - this.testType = Converters.getImplicitConverter(TestType.class).convert(testType); - } - } + } + if (!Objects.equals(excludePattern, appPropertiesExcludePattern)) { + if (excludePattern == null) { + this.exclude = null; + } else { + this.exclude = Pattern.compile(excludePattern); } - appPropertiesIncludeTags = includeTags; - appPropertiesExcludeTags = excludeTags; - appPropertiesIncludePattern = includePattern; - appPropertiesExcludePattern = excludePattern; - appPropertiesIncludeEngines = includeEngines; - appPropertiesExcludeEngines = excludeEngines; - appPropertiesTestType = testType; - break; + } + if (!Objects.equals(includeEngines, appPropertiesIncludeEngines)) { + this.includeEngines = Objects.requireNonNullElse(includeEngines, Collections.emptyList()); + } + if (!Objects.equals(excludeEngines, appPropertiesExcludeEngines)) { + this.excludeEngines = Objects.requireNonNullElse(excludeEngines, Collections.emptyList()); + } + if (!Objects.equals(testType, appPropertiesTestType)) { + this.testType = testType; } } + + appPropertiesIncludeTags = includeTags; + appPropertiesExcludeTags = excludeTags; + appPropertiesIncludePattern = includePattern; + appPropertiesExcludePattern = excludePattern; + appPropertiesIncludeEngines = includeEngines; + appPropertiesExcludeEngines = excludeEngines; + appPropertiesTestType = testType; + } + + private static SmallRyeConfig getMinimalConfig() { + return new SmallRyeConfigBuilder().addDefaultSources().build(); + } + + private Optional> getTrimmedListFromConfig(SmallRyeConfig updatedConfig, String property) { + return updatedConfig.getOptionalValue(property, String.class) + .map(t -> Arrays.stream(t.split(",")).map(String::trim) + .collect(Collectors.toList())); } public boolean isStarted() { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java index 8ff20a9bcf92da..fead5a43bbdf08 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java @@ -25,7 +25,6 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.IsNormal; @@ -155,13 +154,8 @@ public ServiceStartBuildItem searchForTags(CombinedIndexBuildItem combinedIndexB return null; } - public boolean isAppClass(String theClassName) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(theClassName.replace(".", "/") + ".class", true); - return !res.isEmpty(); + public boolean isAppClass(String className) { + return QuarkusClassLoader.isApplicationClass(className); } public static class TracingClassVisitor extends ClassVisitor { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ide/IdeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/ide/IdeConfig.java index 4bbc0aa105d52e..f98cf00ca5ab17 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ide/IdeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ide/IdeConfig.java @@ -3,6 +3,9 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * IDE + */ @ConfigRoot public class IdeConfig { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/images/ContainerImages.java b/core/deployment/src/main/java/io/quarkus/deployment/images/ContainerImages.java new file mode 100644 index 00000000000000..7d1be878aa593b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/images/ContainerImages.java @@ -0,0 +1,114 @@ +package io.quarkus.deployment.images; + +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; + +/** + * This class is used to define the container images that are used by Quarkus. + *

+ * For each image, the image name and version are defined as constants: + *

+ * - {@code x_IMAGE_NAME} - the name of the image without the version (e.g. {@code registry.access.redhat.com/ubi8/ubi-minimal}) + * - {@code x_VERSION} - the version of the image (e.g. {@code 8.10}) + * - {@code x} - the full image name (e.g. {@code registry.access.redhat.com/ubi8/ubi-minimal:8.10}) + */ +public class ContainerImages { + + // Global versions + + /** + * UBI 8 version + */ + public static final String UBI8_VERSION = "8.10"; + + /** + * UBI 8 version + */ + public static final String UBI9_VERSION = "9.4"; + + /** + * Version used for more UBI Java images. + */ + public static final String UBI8_JAVA_VERSION = "1.20"; + + /** + * Version uses for the native builder image. + */ + public static final String NATIVE_BUILDER_VERSION = "jdk-21"; + + // === Runtime images for containers (native) + + // UBI 8 Minimal - https://catalog.redhat.com/software/containers/ubi8/ubi-minimal/5c359a62bed8bd75a2c3fba8 + public static final String UBI8_MINIMAL_IMAGE_NAME = "registry.access.redhat.com/ubi8/ubi-minimal"; + public static final String UBI8_MINIMAL_VERSION = UBI8_VERSION; + public static final String UBI8_MINIMAL = UBI8_MINIMAL_IMAGE_NAME + ":" + UBI8_MINIMAL_VERSION; + + // UBI 9 Minimal - https://catalog.redhat.com/software/containers/ubi9-minimal/61832888c0d15aff4912fe0d + public static final String UBI9_MINIMAL_IMAGE_NAME = "registry.access.redhat.com/ubi9/ubi-minimal"; + public static final String UBI9_MINIMAL_VERSION = UBI9_VERSION; + public static final String UBI9_MINIMAL = UBI9_MINIMAL_IMAGE_NAME + ":" + UBI9_MINIMAL_VERSION; + + // Quarkus Micro image - https://quay.io/repository/quarkus/quarkus-micro-image?tab=tags + public static final String QUARKUS_MICRO_IMAGE_NAME = "quay.io/quarkus/quarkus-micro-image"; + public static final String QUARKUS_MICRO_VERSION = "2.0"; + public static final String QUARKUS_MICRO_IMAGE = QUARKUS_MICRO_IMAGE_NAME + ":" + QUARKUS_MICRO_VERSION; + + // === Runtime images for containers (JVM) + + // UBI 8 OpenJDK 17 Runtime - https://catalog.redhat.com/software/containers/ubi8/openjdk-17-runtime/618bdc5f843af1624c4e4ba8 + public static final String UBI8_JAVA_17_IMAGE_NAME = "registry.access.redhat.com/ubi8/openjdk-17-runtime"; + public static final String UBI8_JAVA_17_VERSION = UBI8_JAVA_VERSION; + public static final String UBI8_JAVA_17 = UBI8_JAVA_17_IMAGE_NAME + ":" + UBI8_JAVA_17_VERSION; + + // UBI 8 OpenJDK 21 Runtime - https://catalog.redhat.com/software/containers/ubi8/openjdk-21-runtime/653fd184292263c0a2f14d69 + public static final String UBI8_JAVA_21_IMAGE_NAME = "registry.access.redhat.com/ubi8/openjdk-21-runtime"; + public static final String UBI8_JAVA_21_VERSION = UBI8_JAVA_VERSION; + public static final String UBI8_JAVA_21 = UBI8_JAVA_21_IMAGE_NAME + ":" + UBI8_JAVA_21_VERSION; + + // UBI 9 OpenJDK 17 Runtime - https://catalog.redhat.com/software/containers/ubi9/openjdk-17-runtime/61ee7d45384a3eb331996bee + public static final String UBI9_JAVA_17_IMAGE_NAME = "registry.access.redhat.com/ubi9/openjdk-17-runtime"; + public static final String UBI9_JAVA_17_VERSION = UBI8_JAVA_VERSION; + public static final String UBI9_JAVA_17 = UBI9_JAVA_17_IMAGE_NAME + ":" + UBI9_JAVA_17_VERSION; + + // UBI 9 OpenJDK 21 Runtime - https://catalog.redhat.com/software/containers/ubi9/openjdk-21-runtime/6501ce769a0d86945c422d5f + public static final String UBI9_JAVA_21_IMAGE_NAME = "registry.access.redhat.com/ubi9/openjdk-21-runtime"; + public static final String UBI9_JAVA_21_VERSION = UBI8_JAVA_VERSION; + public static final String UBI9_JAVA_21 = UBI9_JAVA_21_IMAGE_NAME + ":" + UBI9_JAVA_21_VERSION; + + // === Source To Image images + + // Quarkus Binary Source To Image - https://quay.io/repository/quarkus/ubi-quarkus-native-binary-s2i?tab=tags + public static final String QUARKUS_BINARY_S2I_IMAGE_NAME = "quay.io/quarkus/ubi-quarkus-native-binary-s2i"; + public static final String QUARKUS_BINARY_S2I_VERSION = "2.0"; + public static final String QUARKUS_BINARY_S2I = QUARKUS_BINARY_S2I_IMAGE_NAME + ":" + QUARKUS_BINARY_S2I_VERSION; + + // Java 17 Source To Image - https://catalog.redhat.com/software/containers/ubi8/openjdk-17/618bdbf34ae3739687568813 + public static final String S2I_JAVA_17_IMAGE_NAME = "registry.access.redhat.com/ubi8/openjdk-17"; + public static final String S2I_JAVA_17_VERSION = UBI8_JAVA_VERSION; + public static final String S2I_JAVA_17 = S2I_JAVA_17_IMAGE_NAME + ":" + S2I_JAVA_17_VERSION; + + // Java Source To Image - https://catalog.redhat.com/software/containers/ubi8/openjdk-21/653fb7e21b2ec10f7dfc10d0?q=openjdk%2021&architecture=amd64&image=66bcc007a3857fbc34f4dce1 + public static final String S2I_JAVA_21_IMAGE_NAME = "registry.access.redhat.com/ubi8/openjdk-21"; + public static final String S2I_JAVA_21_VERSION = UBI8_JAVA_VERSION; + public static final String S2I_JAVA_21 = S2I_JAVA_21_IMAGE_NAME + ":" + S2I_JAVA_21_VERSION; + + // === Native Builder images + + // Mandrel Builder Image - https://quay.io/repository/quarkus/ubi-quarkus-mandrel-builder-image?tab=tags + public static final String MANDREL_BUILDER_IMAGE_NAME = "quay.io/quarkus/ubi-quarkus-mandrel-builder-image"; + public static final String MANDREL_BUILDER_VERSION = NATIVE_BUILDER_VERSION; + public static final String MANDREL_BUILDER = MANDREL_BUILDER_IMAGE_NAME + ":" + MANDREL_BUILDER_VERSION; + + // GraalVM CE Builder Image - https://quay.io/repository/quarkus/ubi-quarkus-graalvmce-builder-image?tab=tags + public static final String GRAALVM_BUILDER_IMAGE_NAME = "quay.io/quarkus/ubi-quarkus-graalvmce-builder-image"; + public static final String GRAALVM_BUILDER_VERSION = NATIVE_BUILDER_VERSION; + public static final String GRAALVM_BUILDER = GRAALVM_BUILDER_IMAGE_NAME + ":" + GRAALVM_BUILDER_VERSION; + + public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { + switch (version.isJava21OrHigher()) { + case TRUE: + return UBI8_JAVA_21; + default: + return UBI8_JAVA_17; + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java index 285fa0051a1aa0..619976d52461e4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java @@ -56,6 +56,9 @@ public class ApplicationArchiveBuildStep { IndexDependencyConfiguration config; + /** + * Indexing + */ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) static final class IndexDependencyConfiguration { /** diff --git a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java index 03d59b48f2d258..3ad7a589dbb010 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.index; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.IOException; import java.io.InputStream; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java index f0289037eea196..9d94bcac054817 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java @@ -40,104 +40,109 @@ public class JBangAugmentorImpl implements BiConsumer resultMap) { - QuarkusClassLoader classLoader = curatedApplication.getAugmentClassLoader(); + QuarkusClassLoader classLoader = curatedApplication.getOrCreateAugmentClassLoader(); - QuarkusBootstrap quarkusBootstrap = curatedApplication.getQuarkusBootstrap(); - QuarkusAugmentor.Builder builder = QuarkusAugmentor.builder() - .setRoot(quarkusBootstrap.getApplicationRoot()) - .setClassLoader(classLoader) - .addFinal(ApplicationClassNameBuildItem.class) - .setTargetDir(quarkusBootstrap.getTargetDirectory()) - .setDeploymentClassLoader(curatedApplication.createDeploymentClassLoader()) - .setBuildSystemProperties(quarkusBootstrap.getBuildSystemProperties()) - .setEffectiveModel(curatedApplication.getApplicationModel()); - if (quarkusBootstrap.getBaseName() != null) { - builder.setBaseName(quarkusBootstrap.getBaseName()); - } - if (quarkusBootstrap.getOriginalBaseName() != null) { - builder.setOriginalBaseName(quarkusBootstrap.getOriginalBaseName()); - } - - boolean auxiliaryApplication = curatedApplication.getQuarkusBootstrap().isAuxiliaryApplication(); - builder.setAuxiliaryApplication(auxiliaryApplication); - builder.setAuxiliaryDevModeType( - curatedApplication.getQuarkusBootstrap().isHostApplicationIsTestOnly() ? DevModeType.TEST_ONLY - : (auxiliaryApplication ? DevModeType.LOCAL : null)); - builder.setLaunchMode(LaunchMode.NORMAL); - builder.setRebuild(quarkusBootstrap.isRebuild()); - builder.setLiveReloadState( - new LiveReloadBuildItem(false, Collections.emptySet(), new HashMap<>(), null)); - for (AdditionalDependency i : quarkusBootstrap.getAdditionalApplicationArchives()) { - //this gets added to the class path either way - //but we only need to add it to the additional app archives - //if it is forced as an app archive - if (i.isForceApplicationArchive()) { - builder.addAdditionalApplicationArchive(i.getResolvedPaths()); + try (QuarkusClassLoader deploymentClassLoader = curatedApplication.createDeploymentClassLoader()) { + QuarkusBootstrap quarkusBootstrap = curatedApplication.getQuarkusBootstrap(); + QuarkusAugmentor.Builder builder = QuarkusAugmentor.builder() + .setRoot(quarkusBootstrap.getApplicationRoot()) + .setClassLoader(classLoader) + .addFinal(ApplicationClassNameBuildItem.class) + .setTargetDir(quarkusBootstrap.getTargetDirectory()) + .setDeploymentClassLoader(curatedApplication.createDeploymentClassLoader()) + .setBuildSystemProperties(quarkusBootstrap.getBuildSystemProperties()) + .setRuntimeProperties(quarkusBootstrap.getRuntimeProperties()) + .setEffectiveModel(curatedApplication.getApplicationModel()); + if (quarkusBootstrap.getBaseName() != null) { + builder.setBaseName(quarkusBootstrap.getBaseName()); } - } - builder.addBuildChainCustomizer(new Consumer() { - @Override - public void accept(BuildChainBuilder builder) { - final BuildStepBuilder stepBuilder = builder.addBuildStep((ctx) -> { - ctx.produce(new ProcessInheritIODisabledBuildItem()); - }); - stepBuilder.produces(ProcessInheritIODisabledBuildItem.class).build(); + if (quarkusBootstrap.getOriginalBaseName() != null) { + builder.setOriginalBaseName(quarkusBootstrap.getOriginalBaseName()); } - }); - builder.excludeFromIndexing(quarkusBootstrap.getExcludeFromClassPath()); - builder.addFinal(GeneratedClassBuildItem.class); - builder.addFinal(MainClassBuildItem.class); - builder.addFinal(GeneratedResourceBuildItem.class); - builder.addFinal(TransformedClassesBuildItem.class); - builder.addFinal(DeploymentResultBuildItem.class); - // note: quarkus.package.type is deprecated - boolean nativeRequested = "native".equals(System.getProperty("quarkus.package.type")) - || "true".equals(System.getProperty("quarkus.native.enabled")); - boolean containerBuildRequested = Boolean.getBoolean("quarkus.container-image.build"); - if (nativeRequested) { - builder.addFinal(NativeImageBuildItem.class); - } - if (containerBuildRequested) { - //TODO: this is a bit ugly - //we don't necessarily need these artifacts - //but if we include them it does mean that you can auto create docker images - //and deploy to kube etc - //for an ordinary build with no native and no docker this is a waste - builder.addFinal(ArtifactResultBuildItem.class); - } - try { - BuildResult buildResult = builder.build().run(); - Map result = new HashMap<>(); - for (GeneratedClassBuildItem i : buildResult.consumeMulti(GeneratedClassBuildItem.class)) { - result.put(i.getName().replace(".", "/") + ".class", i.getClassData()); + boolean auxiliaryApplication = curatedApplication.getQuarkusBootstrap().isAuxiliaryApplication(); + builder.setAuxiliaryApplication(auxiliaryApplication); + builder.setAuxiliaryDevModeType( + curatedApplication.getQuarkusBootstrap().isHostApplicationIsTestOnly() ? DevModeType.TEST_ONLY + : (auxiliaryApplication ? DevModeType.LOCAL : null)); + builder.setLaunchMode(LaunchMode.NORMAL); + builder.setRebuild(quarkusBootstrap.isRebuild()); + builder.setLiveReloadState( + new LiveReloadBuildItem(false, Collections.emptySet(), new HashMap<>(), null)); + for (AdditionalDependency i : quarkusBootstrap.getAdditionalApplicationArchives()) { + //this gets added to the class path either way + //but we only need to add it to the additional app archives + //if it is forced as an app archive + if (i.isForceApplicationArchive()) { + builder.addAdditionalApplicationArchive(i.getResolvedPaths()); + } + } + builder.addBuildChainCustomizer(new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + final BuildStepBuilder stepBuilder = builder.addBuildStep((ctx) -> { + ctx.produce(new ProcessInheritIODisabledBuildItem()); + }); + stepBuilder.produces(ProcessInheritIODisabledBuildItem.class).build(); + } + }); + builder.excludeFromIndexing(quarkusBootstrap.getExcludeFromClassPath()); + builder.addFinal(GeneratedClassBuildItem.class); + builder.addFinal(MainClassBuildItem.class); + builder.addFinal(GeneratedResourceBuildItem.class); + builder.addFinal(TransformedClassesBuildItem.class); + builder.addFinal(DeploymentResultBuildItem.class); + // note: quarkus.package.type is deprecated + boolean nativeRequested = "native".equals(System.getProperty("quarkus.package.type")) + || "true".equals(System.getProperty("quarkus.native.enabled")); + boolean containerBuildRequested = Boolean.getBoolean("quarkus.container-image.build"); + if (nativeRequested) { + builder.addFinal(NativeImageBuildItem.class); } - for (GeneratedResourceBuildItem i : buildResult.consumeMulti(GeneratedResourceBuildItem.class)) { - result.put(i.getName(), i.getData()); + if (containerBuildRequested) { + //TODO: this is a bit ugly + //we don't necessarily need these artifacts + //but if we include them it does mean that you can auto create docker images + //and deploy to kube etc + //for an ordinary build with no native and no docker this is a waste + builder.addFinal(ArtifactResultBuildItem.class); } - for (Map.Entry> entry : buildResult - .consume(TransformedClassesBuildItem.class).getTransformedClassesByJar().entrySet()) { - for (TransformedClassesBuildItem.TransformedClass transformed : entry.getValue()) { - if (transformed.getData() != null) { - result.put(transformed.getFileName(), transformed.getData()); - } else { - log.warn("Unable to remove resource " + transformed.getFileName() - + " as this is not supported in JBangf"); + + try { + BuildResult buildResult = builder.build().run(); + Map result = new HashMap<>(); + for (GeneratedClassBuildItem i : buildResult.consumeMulti(GeneratedClassBuildItem.class)) { + result.put(i.getName().replace(".", "/") + ".class", i.getClassData()); + } + for (GeneratedResourceBuildItem i : buildResult.consumeMulti(GeneratedResourceBuildItem.class)) { + result.put(i.getName(), i.getData()); + } + for (Map.Entry> entry : buildResult + .consume(TransformedClassesBuildItem.class).getTransformedClassesByJar().entrySet()) { + for (TransformedClassesBuildItem.TransformedClass transformed : entry.getValue()) { + if (transformed.getData() != null) { + result.put(transformed.getFileName(), transformed.getData()); + } else { + log.warn("Unable to remove resource " + transformed.getFileName() + + " as this is not supported in JBangf"); + } } } + resultMap.put("files", result); + final List javaargs = new ArrayList<>(); + javaargs.add("-Djava.util.logging.manager=org.jboss.logmanager.LogManager"); + javaargs.add( + "-Djava.util.concurrent.ForkJoinPool.common.threadFactory=io.quarkus.bootstrap.forkjoin.QuarkusForkJoinWorkerThreadFactory"); + resultMap.put("java-args", javaargs); + resultMap.put("main-class", buildResult.consume(MainClassBuildItem.class).getClassName()); + if (nativeRequested) { + resultMap.put("native-image", buildResult.consume(NativeImageBuildItem.class).getPath()); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); } - resultMap.put("files", result); - final List javaargs = new ArrayList<>(); - javaargs.add("-Djava.util.logging.manager=org.jboss.logmanager.LogManager"); - javaargs.add( - "-Djava.util.concurrent.ForkJoinPool.common.threadFactory=io.quarkus.bootstrap.forkjoin.QuarkusForkJoinWorkerThreadFactory"); - resultMap.put("java-args", javaargs); - resultMap.put("main-class", buildResult.consume(MainClassBuildItem.class).getClassName()); - if (nativeRequested) { - resultMap.put("native-image", buildResult.consume(NativeImageBuildItem.class).getPath()); - } - } catch (Exception e) { - throw new RuntimeException(e); } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java new file mode 100644 index 00000000000000..500a84f90b0f17 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java @@ -0,0 +1,45 @@ +package io.quarkus.deployment.logging; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.CompositeIndex; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Contains information to decorate the Log output. Can be used by extensions that output the log / stacktraces, + * for example the error page. + * + * Also see io.quarkus.runtime.logging.DecorateStackUtil to assist with the decoration + */ +public final class LoggingDecorateBuildItem extends SimpleBuildItem { + private final Path srcMainJava; + private final CompositeIndex knowClassesIndex; + + public LoggingDecorateBuildItem(Path srcMainJava, CompositeIndex knowClassesIndex) { + this.srcMainJava = srcMainJava; + this.knowClassesIndex = knowClassesIndex; + } + + public Path getSrcMainJava() { + return srcMainJava; + } + + public CompositeIndex getKnowClassesIndex() { + return knowClassesIndex; + } + + public List getKnowClasses() { + List knowClasses = new ArrayList<>(); + Collection knownClasses = knowClassesIndex.getKnownClasses(); + for (ClassInfo ci : knownClasses) { + knowClasses.add(ci.name().toString()); + } + return knowClasses; + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index 4bc1ee15c7e94f..cec9fc34fd9e15 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -1,7 +1,10 @@ package io.quarkus.deployment.logging; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -39,12 +42,15 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.logging.Logger; +import org.jboss.logmanager.ExtLogRecord; import org.jboss.logmanager.LogContextInitializer; import org.jboss.logmanager.LogManager; import org.objectweb.asm.Opcodes; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.workspace.WorkspaceModule; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.IsNormal; @@ -86,6 +92,8 @@ import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.metrics.MetricsFactoryConsumerBuildItem; import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.JandexUtil; @@ -102,11 +110,13 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.logging.LoggingFilter; +import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigInstantiator; import io.quarkus.runtime.console.ConsoleRuntimeConfig; import io.quarkus.runtime.logging.CategoryBuildTimeConfig; import io.quarkus.runtime.logging.CleanupFilterConfig; +import io.quarkus.runtime.logging.DecorateStackUtil; import io.quarkus.runtime.logging.DiscoveredLogComponents; import io.quarkus.runtime.logging.InheritableLevel; import io.quarkus.runtime.logging.LogBuildTimeConfig; @@ -215,6 +225,7 @@ void miscSetup( Consumer provider) { runtimeInit.accept(new RuntimeReinitializedClassBuildItem(ConsoleHandler.class.getName())); runtimeInit.accept(new RuntimeReinitializedClassBuildItem("io.smallrye.common.ref.References$ReaperThread")); + runtimeInit.accept(new RuntimeReinitializedClassBuildItem("io.smallrye.common.os.Process")); systemProp .accept(new NativeImageSystemPropertyBuildItem("java.util.logging.manager", "org.jboss.logmanager.LogManager")); provider.accept( @@ -282,7 +293,7 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe if (!discoveredLogComponents.getNameToFilterClass().isEmpty()) { reflectiveClassBuildItemBuildProducer.produce( ReflectiveClassBuildItem.builder(discoveredLogComponents.getNameToFilterClass().values().toArray( - EMPTY_STRING_ARRAY)).build()); + EMPTY_STRING_ARRAY)).reason(getClass().getName()).build()); serviceProviderBuildItemBuildProducer .produce(ServiceProviderBuildItem.allProvidersFromClassPath(LogFilterFactory.class.getName())); } @@ -370,14 +381,25 @@ private DiscoveredLogComponents discoverLogComponents(IndexView index) { void setupStackTraceFormatter(ApplicationArchivesBuildItem item, EffectiveIdeBuildItem ideSupport, BuildSystemTargetBuildItem buildSystemTargetBuildItem, List exceptionNotificationBuildItems, - CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem) { + CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem, + CurateOutcomeBuildItem curateOutcomeBuildItem, + OutputTargetBuildItem outputTargetBuildItem, + LaunchModeBuildItem launchMode, + LogBuildTimeConfig logBuildTimeConfig, + BuildProducer loggingDecorateProducer) { List indexList = new ArrayList<>(); for (ApplicationArchive i : item.getAllApplicationArchives()) { if (i.getResolvedPaths().isSinglePath() && Files.isDirectory(i.getResolvedPaths().getSinglePath())) { indexList.add(i.getIndex()); } } + Path srcMainJava = getSourceRoot(curateOutcomeBuildItem.getApplicationModel(), + outputTargetBuildItem.getOutputDirectory()); + CompositeIndex index = CompositeIndex.create(indexList); + + loggingDecorateProducer.produce(new LoggingDecorateBuildItem(srcMainJava, index)); + //awesome/horrible hack //we know from the index which classes are part of the current application //we add ANSI codes for bold and underline to their names to display them more prominently @@ -393,6 +415,47 @@ public void accept(LogRecord logRecord, Consumer logRecordConsumer) { var elem = stackTrace[i]; if (index.getClassByName(DotName.createSimple(elem.getClassName())) != null) { lastUserCode = stackTrace[i]; + + if (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT) + && logBuildTimeConfig.decorateStacktraces) { + + String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem); + if (decoratedString != null) { + if (logRecord instanceof ExtLogRecord elr) { + switch (elr.getFormatStyle()) { + case MESSAGE_FORMAT -> { + Object[] p = elr.getParameters(); // can be null + Object[] np = p != null ? Arrays.copyOf(p, p.length + 1) : new Object[1]; + np[np.length - 1] = decoratedString; + elr.setParameters(np); + elr.setMessage(elr.getMessage() + "\n\n{" + (np.length - 1) + "}\n\n"); + } + case PRINTF -> { + Object[] p = elr.getParameters(); // can be null + Object[] np = p != null ? Arrays.copyOf(p, p.length + 1) : new Object[1]; + np[np.length - 1] = decoratedString; + elr.setParameters(np); + elr.setMessage(elr.getMessage() + "\n\n%" + (np.length - 1) + "$s", + ExtLogRecord.FormatStyle.PRINTF); + } + case NO_FORMAT -> { + elr.setParameters(new Object[] { + elr.getMessage(), + decoratedString + }); + elr.setMessage("{0}\n\n{1}\n\n"); + } + } + } else { + Object[] p = logRecord.getParameters(); // can be null + Object[] np = p != null ? Arrays.copyOf(p, p.length + 1) : new Object[1]; + np[np.length - 1] = decoratedString; + logRecord.setParameters(np); + logRecord.setMessage(logRecord.getMessage() + "\n\n{" + (np.length - 1) + "}\n\n"); + } + } + } + stackTrace[i] = new StackTraceElement(elem.getClassLoaderName(), elem.getModuleName(), elem.getModuleVersion(), MessageFormat.UNDERLINE + MessageFormat.BOLD + elem.getClassName() @@ -665,6 +728,24 @@ ConsoleCommandBuildItem logConsoleCommand() { return new ConsoleCommandBuildItem(new LogCommand()); } + private Path getSourceRoot(ApplicationModel applicationModel, Path target) { + WorkspaceModule workspaceModule = applicationModel.getAppArtifact().getWorkspaceModule(); + if (workspaceModule != null) { + return workspaceModule.getModuleDir().toPath().resolve(SRC_MAIN_JAVA); + } + + if (target != null) { + var baseDir = target.getParent(); + if (baseDir == null) { + baseDir = target; + } + return baseDir.resolve(SRC_MAIN_JAVA); + } + return Paths.get(SRC_MAIN_JAVA); + } + + private static final String SRC_MAIN_JAVA = "src/main/java"; + @GroupCommandDefinition(name = "log", description = "Logging Commands") public static class LogCommand implements GroupCommand { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/naming/NamingConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/naming/NamingConfig.java index 976482c146f3f4..e764ec31c1daf2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/naming/NamingConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/naming/NamingConfig.java @@ -3,6 +3,9 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Naming + */ @ConfigRoot public class NamingConfig { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java index 310090fd7c1eeb..12c4728038d71a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.deployment.images.ContainerImages; import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigGroup; @@ -16,13 +17,13 @@ import io.smallrye.config.WithDefault; import io.smallrye.config.WithParentName; +/** + * Native executables + */ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) @ConfigMapping(prefix = "quarkus.native") public interface NativeConfig { - String DEFAULT_GRAALVM_BUILDER_IMAGE = "quay.io/quarkus/ubi-quarkus-graalvmce-builder-image:jdk-21"; - String DEFAULT_MANDREL_BUILDER_IMAGE = "quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21"; - /** * Set to enable native-image building using GraalVM. */ @@ -122,7 +123,12 @@ public interface NativeConfig { String fileEncoding(); /** - * If all character sets should be added to the native image. This increases image size + * If all character sets should be added to the native executable. + *

+ * Note that some extensions (e.g. the Oracle JDBC driver) also take this setting into account to enable support for all + * charsets at the extension level. + *

+ * This increases image size. */ @WithDefault("false") boolean addAllCharsets(); @@ -270,9 +276,9 @@ interface BuilderImageConfig { default String getEffectiveImage() { final String builderImageName = this.image().toUpperCase(); if (builderImageName.equals(BuilderImageProvider.GRAALVM.name())) { - return DEFAULT_GRAALVM_BUILDER_IMAGE; + return ContainerImages.GRAALVM_BUILDER; } else if (builderImageName.equals(BuilderImageProvider.MANDREL.name())) { - return DEFAULT_MANDREL_BUILDER_IMAGE; + return ContainerImages.MANDREL_BUILDER; } else { return this.image(); } @@ -337,6 +343,11 @@ default String getEffectiveImage() { * If errors should be reported at runtime. This is a more relaxed setting, however it is not recommended as it * means * your application may fail at runtime if an unsupported feature is used by accident. + * + * Note that the use of this flag may result in build time failures due to {@code ClassNotFoundException}s. + * Reason most likely being that the Quarkus extension already optimized it away or do not actually need it. + * In such cases you should explicitly add the corresponding dependency providing the missing classes as a + * dependency to your project. */ @WithDefault("false") boolean reportErrorsAtRuntime(); @@ -481,11 +492,30 @@ interface Debug { @WithDefault("false") boolean enableDashboardDump(); + /** + * Include a reasons entries in the generated json configuration files. + */ + @WithDefault("false") + boolean includeReasonsInConfigFiles(); + /** * Configure native executable compression using UPX. */ Compression compression(); + /** + * Configuration files generated by the Quarkus build, using native image agent, are informative by default. + * In other words, the generated configuration files are presented in the build log but are not applied. + * When this option is set to true, generated configuration files are applied to the native executable building process. + *

+ * Enabling this option should be done with care, because it can make native image configuration and/or behaviour + * dependant on other non-obvious factors. For example, if the native image agent generated configuration was generated + * from running JVM unit tests, disabling test(s) can result in a different native image configuration being generated, + * which in turn can misconfigure the native executable or affect its behaviour in unintended ways. + */ + @WithDefault("false") + boolean agentConfigurationApply(); + @ConfigGroup interface Compression { /** diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index 57c9f320391135..0e7594bf521a5f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -16,6 +16,8 @@ import io.smallrye.config.WithDefault; /** + * Packaging the application + *

* Configuration relating to creating a packaged output. */ @ConfigMapping(prefix = "quarkus.package") @@ -344,11 +346,19 @@ public static JarType fromString(String value) { @ConfigGroup interface DecompilerConfig { /** - * Enable decompilation of generated and transformed bytecode into the `decompiled` directory. + * Enable decompilation of generated and transformed bytecode into a filesystem. */ @WithDefault("false") boolean enabled(); + /** + * The directory into which to save the decompilation output. + *

+ * A relative path is understood as relative to the build directory. + */ + @WithDefault("decompiler") + String outputDirectory(); + /** * The directory into which to save the decompilation tool if it doesn't exist locally. */ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java index a96468ab8b83ae..1c1b60fafc88b1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/ArtifactResultBuildItem.java @@ -4,6 +4,7 @@ import java.util.Map; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.sbom.ApplicationManifestConfig; /** * Represents a runnable artifact, such as an uberjar or thin jar. @@ -17,11 +18,18 @@ public final class ArtifactResultBuildItem extends MultiBuildItem { private final Path path; private final String type; private final Map metadata; + private final ApplicationManifestConfig manifestConfig; public ArtifactResultBuildItem(Path path, String type, Map metadata) { + this(path, type, metadata, null); + } + + public ArtifactResultBuildItem(Path path, String type, Map metadata, + ApplicationManifestConfig manifestConfig) { this.path = path; this.type = type; this.metadata = metadata; + this.manifestConfig = manifestConfig; } public Path getPath() { @@ -32,6 +40,10 @@ public String getType() { return type; } + public ApplicationManifestConfig getManifestConfig() { + return manifestConfig; + } + public Map getMetadata() { return metadata; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java index 8eb4a30d84d435..ba5a0e5601bb37 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/JarBuildItem.java @@ -3,10 +3,13 @@ import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.*; import java.nio.file.Path; +import java.util.Collection; import io.quarkus.bootstrap.app.JarResult; +import io.quarkus.bootstrap.app.SbomResult; import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.sbom.ApplicationManifestConfig; public final class JarBuildItem extends SimpleBuildItem { @@ -15,14 +18,21 @@ public final class JarBuildItem extends SimpleBuildItem { private final Path libraryDir; private final PackageConfig.JarConfig.JarType type; private final String classifier; + private final ApplicationManifestConfig manifestConfig; public JarBuildItem(Path path, Path originalArtifact, Path libraryDir, PackageConfig.JarConfig.JarType type, String classifier) { + this(path, originalArtifact, libraryDir, type, classifier, null); + } + + public JarBuildItem(Path path, Path originalArtifact, Path libraryDir, PackageConfig.JarConfig.JarType type, + String classifier, ApplicationManifestConfig manifestConfig) { this.path = path; this.originalArtifact = originalArtifact; this.libraryDir = libraryDir; this.type = type; this.classifier = classifier; + this.manifestConfig = manifestConfig; } public boolean isUberJar() { @@ -49,8 +59,16 @@ public String getClassifier() { return classifier; } + public ApplicationManifestConfig getManifestConfig() { + return manifestConfig; + } + public JarResult toJarResult() { + return toJarResult(null); + } + + public JarResult toJarResult(Collection sboms) { return new JarResult(path, originalArtifact, libraryDir, type == MUTABLE_JAR, - classifier); + classifier, sboms); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/NativeImageBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/NativeImageBuildItem.java index 8a140099907ff5..24c78b3f2c2784 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/NativeImageBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/NativeImageBuildItem.java @@ -10,10 +10,12 @@ public final class NativeImageBuildItem extends SimpleBuildItem { private final Path path; private final GraalVMVersion graalVMVersion; + private final boolean reused; - public NativeImageBuildItem(Path path, GraalVMVersion graalVMVersion) { + public NativeImageBuildItem(Path path, GraalVMVersion graalVMVersion, boolean reused) { this.path = path; this.graalVMVersion = graalVMVersion; + this.reused = reused; } public Path getPath() { @@ -24,6 +26,10 @@ public GraalVMVersion getGraalVMInfo() { return graalVMVersion; } + public boolean isReused() { + return reused; + } + public static class GraalVMVersion { private final String fullVersion; private final String version; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java index 734575f2f0333b..2aa16d5165c0c7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java @@ -15,6 +15,7 @@ public final class GraalVM { // Implements version parsing after https://github.com/oracle/graal/pull/6302 static final class VersionParseHelper { + private static final String EA_BUILD_PREFIX = "-ea"; private static final String JVMCI_BUILD_PREFIX = "jvmci-"; private static final String MANDREL_VERS_PREFIX = "Mandrel-"; @@ -103,11 +104,10 @@ private static String libericaVersion(String vendorVersion) { if (vendorVersion == null) { return null; } - int idx = vendorVersion.indexOf(LIBERICA_NIK_VERS_PREFIX); - if (idx < 0) { + final String version = buildVersion(vendorVersion, LIBERICA_NIK_VERS_PREFIX); + if (version == null) { return null; } - String version = vendorVersion.substring(idx + LIBERICA_NIK_VERS_PREFIX.length()); return matchVersion(version); } @@ -122,11 +122,10 @@ private static String mandrelVersion(String vendorVersion) { if (vendorVersion == null) { return null; } - int idx = vendorVersion.indexOf(MANDREL_VERS_PREFIX); - if (idx < 0) { + final String version = buildVersion(vendorVersion, MANDREL_VERS_PREFIX); + if (version == null) { return null; } - String version = vendorVersion.substring(idx + MANDREL_VERS_PREFIX.length()); return matchVersion(version); } @@ -142,11 +141,13 @@ private static String graalVersion(String buildInfo, int jdkFeature) { if (buildInfo == null) { return null; } - int idx = buildInfo.indexOf(JVMCI_BUILD_PREFIX); - if (idx < 0) { - return null; + String version = buildVersion(buildInfo, JVMCI_BUILD_PREFIX); + if (version == null) { + version = buildVersion(buildInfo, EA_BUILD_PREFIX); + if (version == null) { + return null; + } } - String version = buildInfo.substring(idx + JVMCI_BUILD_PREFIX.length()); Matcher versMatcher = VERSION_PATTERN.matcher(version); if (versMatcher.find()) { return matchVersion(version); @@ -154,6 +155,14 @@ private static String graalVersion(String buildInfo, int jdkFeature) { return GRAAL_MAPPING.get(jdkFeature); } } + + private static String buildVersion(String buildInfo, String buildPrefix) { + int idx = buildInfo.indexOf(buildPrefix); + if (idx < 0) { + return null; + } + return buildInfo.substring(idx + buildPrefix.length()); + } } // Temporarily work around https://github.com/quarkusio/quarkus/issues/36246, @@ -161,8 +170,8 @@ private static String graalVersion(String buildInfo, int jdkFeature) { // https://github.com/quarkusio/quarkus/issues/34161 private static final Map GRAAL_MAPPING = Map.of(22, "24.0", 23, "24.1", - 24, "25.0", - 25, "25.1"); + 24, "24.2", + 25, "25.0"); public static final class Version implements Comparable { 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 5f37c1a83446de..fdf1ed22632daa 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 @@ -1,7 +1,9 @@ package io.quarkus.deployment.pkg.steps; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; -import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.*; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; +import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.LEGACY_JAR; +import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.MUTABLE_JAR; +import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.UBER_JAR; import java.io.BufferedInputStream; import java.io.BufferedWriter; @@ -86,8 +88,11 @@ import io.quarkus.maven.dependency.DependencyFlags; import io.quarkus.maven.dependency.GACT; import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; import io.quarkus.paths.PathVisit; import io.quarkus.paths.PathVisitor; +import io.quarkus.sbom.ApplicationComponent; +import io.quarkus.sbom.ApplicationManifestConfig; import io.quarkus.utilities.JavaBinFinder; /** @@ -154,7 +159,7 @@ public boolean test(String path) { public static final String DEFAULT_FAST_JAR_DIRECTORY_NAME = "quarkus-app"; public static final String MP_CONFIG_FILE = "META-INF/microprofile-config.properties"; - private static final String VINEFLOWER_VERSION = "1.9.3"; + private static final String VINEFLOWER_VERSION = "1.10.1"; @BuildStep OutputTargetBuildItem outputTarget(BuildSystemTargetBuildItem bst, PackageConfig packageConfig) { @@ -175,12 +180,10 @@ OutputTargetBuildItem outputTarget(BuildSystemTargetBuildItem bst, PackageConfig @BuildStep(onlyIf = JarRequired.class) ArtifactResultBuildItem jarOutput(JarBuildItem jarBuildItem) { - if (jarBuildItem.getLibraryDir() != null) { - return new ArtifactResultBuildItem(jarBuildItem.getPath(), "jar", - Collections.singletonMap("library-dir", jarBuildItem.getLibraryDir().toString())); - } else { - return new ArtifactResultBuildItem(jarBuildItem.getPath(), "jar", Collections.emptyMap()); - } + return new ArtifactResultBuildItem(jarBuildItem.getPath(), "jar", + jarBuildItem.getLibraryDir() == null ? Map.of() + : Map.of("library-dir", jarBuildItem.getLibraryDir().toString()), + jarBuildItem.getManifestConfig()); } @SuppressWarnings("deprecation") // JarType#LEGACY_JAR @@ -310,8 +313,31 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, .resolve(outputTargetBuildItem.getOriginalBaseName() + DOT_JAR); final Path originalJar = Files.exists(standardJar) ? standardJar : null; + ResolvedDependency appArtifact = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); + final String classifier = suffixToClassifier(packageConfig.computedRunnerSuffix()); + if (classifier != null && !classifier.isEmpty()) { + appArtifact = ResolvedDependencyBuilder.newInstance() + .setGroupId(appArtifact.getGroupId()) + .setArtifactId(appArtifact.getArtifactId()) + .setClassifier(classifier) + .setType(appArtifact.getType()) + .setVersion(appArtifact.getVersion()) + .setResolvedPaths(appArtifact.getResolvedPaths()) + .addDependencies(appArtifact.getDependencies()) + .setWorkspaceModule(appArtifact.getWorkspaceModule()) + .setFlags(appArtifact.getFlags()) + .build(); + } + final ApplicationManifestConfig manifestConfig = ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setMainComponent(ApplicationComponent.builder() + .setPath(runnerJar) + .setResolvedDependency(appArtifact) + .build()) + .setRunnerPath(runnerJar) + .build(); return new JarBuildItem(runnerJar, originalJar, null, UBER_JAR, - suffixToClassifier(packageConfig.computedRunnerSuffix())); + suffixToClassifier(packageConfig.computedRunnerSuffix()), manifestConfig); } private String suffixToClassifier(String suffix) { @@ -356,16 +382,14 @@ public boolean test(String path) { } }; - final Collection appDeps = curateOutcomeBuildItem.getApplicationModel() - .getRuntimeDependencies(); - ResolvedDependency appArtifact = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); + // the manifest needs to be the first entry in the jar, otherwise JarInputStream does not work properly // see https://bugs.openjdk.java.net/browse/JDK-8031748 generateManifest(runnerZipFs, "", packageConfig, appArtifact, mainClassBuildItem.getClassName(), applicationInfo); - for (ResolvedDependency appDep : appDeps) { + for (ResolvedDependency appDep : curateOutcomeBuildItem.getApplicationModel().getRuntimeDependencies()) { // Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852) // and are not part of the optional dependencies to include @@ -570,6 +594,9 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, buildDir = outputTargetBuildItem.getOutputDirectory().resolve(DEFAULT_FAST_JAR_DIRECTORY_NAME); } + final ApplicationManifestConfig.Builder manifestConfig = ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setDistributionDirectory(buildDir); //unmodified 3rd party dependencies Path libDir = buildDir.resolve(LIB); Path mainLib = libDir.resolve(MAIN); @@ -608,7 +635,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, Decompiler decompiler = null; PackageConfig.DecompilerConfig decompilerConfig = packageConfig.jar().decompiler(); if (decompilerConfig.enabled()) { - decompiledOutputDir = buildDir.getParent().resolve("decompiled"); + decompiledOutputDir = buildDir.getParent().resolve(decompilerConfig.outputDirectory()); FileUtil.deleteDirectory(decompiledOutputDir); Files.createDirectory(decompiledOutputDir); decompiler = new Decompiler.VineflowerDecompiler(); @@ -680,7 +707,6 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, if (!rebuild) { Predicate ignoredEntriesPredicate = getThinJarIgnoredEntriesPredicate(packageConfig); - try (FileSystem runnerZipFs = createNewZip(runnerJar, packageConfig)) { copyFiles(applicationArchivesBuildItem.getRootArchive(), runnerZipFs, null, ignoredEntriesPredicate); } @@ -693,7 +719,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, if (!rebuild) { copyDependency(parentFirstKeys, outputTargetBuildItem, copiedArtifacts, mainLib, baseLib, fastJarJarsBuilder::addDep, true, - classPath, appDep, transformedClasses, removed, packageConfig); + classPath, appDep, transformedClasses, removed, packageConfig, manifestConfig); } else if (includeAppDep(appDep, outputTargetBuildItem.getIncludedOptionalDependencies(), removed)) { appDep.getResolvedPaths().forEach(fastJarJarsBuilder::addDep); } @@ -750,6 +776,8 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, runnerJar.toFile().setReadable(true, false); Path initJar = buildDir.resolve(QUARKUS_RUN_JAR); + manifestConfig.setMainComponent(ApplicationComponent.builder().setPath(initJar)) + .setRunnerPath(initJar); boolean mutableJar = packageConfig.jar().type() == MUTABLE_JAR; if (mutableJar) { //we output the properties in a reproducible manner, so we remove the date comment @@ -761,6 +789,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, List lines = Arrays.stream(out.toString(StandardCharsets.UTF_8).split("\n")) .filter(s -> !s.startsWith("#")).sorted().collect(Collectors.toList()); Path buildSystemProps = quarkus.resolve(BUILD_SYSTEM_PROPERTIES); + manifestConfig.addComponent(ApplicationComponent.builder().setPath(buildSystemProps).setDevelopmentScope()); try (OutputStream fileOutput = Files.newOutputStream(buildSystemProps)) { fileOutput.write(String.join("\n", lines).getBytes(StandardCharsets.UTF_8)); } @@ -778,10 +807,9 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, Path deploymentLib = libDir.resolve(DEPLOYMENT_LIB); Files.createDirectories(deploymentLib); for (ResolvedDependency appDep : curateOutcomeBuildItem.getApplicationModel().getDependencies()) { - copyDependency(parentFirstKeys, outputTargetBuildItem, copiedArtifacts, deploymentLib, baseLib, (p) -> { - }, - false, classPath, - appDep, new TransformedClassesBuildItem(Map.of()), removed, packageConfig); //we don't care about transformation here, so just pass in an empty item + copyDependency(parentFirstKeys, outputTargetBuildItem, copiedArtifacts, deploymentLib, baseLib, p -> { + }, false, classPath, appDep, new TransformedClassesBuildItem(Map.of()), removed, packageConfig, + manifestConfig); //we don't care about transformation here, so just pass in an empty item } Map> relativePaths = new HashMap<>(); for (Map.Entry> e : copiedArtifacts.entrySet()) { @@ -797,6 +825,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, curateOutcomeBuildItem.getApplicationModel(), packageConfig.jar().userProvidersDirectory().orElse(null), buildDir.relativize(runnerJar).toString()); Path appmodelDat = deploymentLib.resolve(APPMODEL_DAT); + manifestConfig.addComponent(ApplicationComponent.builder().setPath(appmodelDat).setDevelopmentScope()); try (OutputStream out = Files.newOutputStream(appmodelDat)) { ObjectOutputStream obj = new ObjectOutputStream(out); obj.writeObject(model); @@ -807,6 +836,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, //as we don't really have a resolved bootstrap CP //once we have the app model it will all be done in QuarkusClassLoader anyway Path deploymentCp = deploymentLib.resolve(DEPLOYMENT_CLASS_PATH_DAT); + manifestConfig.addComponent(ApplicationComponent.builder().setPath(deploymentCp).setDevelopmentScope()); try (OutputStream out = Files.newOutputStream(deploymentCp)) { ObjectOutputStream obj = new ObjectOutputStream(out); List paths = new ArrayList<>(); @@ -842,7 +872,7 @@ public void accept(Path path) { } }); } - return new JarBuildItem(initJar, null, libDir, packageConfig.jar().type(), null); + return new JarBuildItem(initJar, null, libDir, packageConfig.jar().type(), null, manifestConfig.build()); } /** @@ -883,7 +913,7 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB Map> runtimeArtifacts, Path libDir, Path baseLib, Consumer targetPathConsumer, boolean allowParentFirst, StringBuilder classPath, ResolvedDependency appDep, TransformedClassesBuildItem transformedClasses, Set removedDeps, - PackageConfig packageConfig) + PackageConfig packageConfig, ApplicationManifestConfig.Builder manifestConfig) throws IOException { // Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852) @@ -923,6 +953,9 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB } } } + var appComponent = ApplicationComponent.builder() + .setPath(targetPath) + .setResolvedDependency(appDep); if (removedFromThisArchive.isEmpty()) { Files.copy(resolvedDep, targetPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); @@ -930,7 +963,16 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB // we copy jars for which we remove entries to the same directory // which seems a bit odd to me filterJarFile(resolvedDep, targetPath, removedFromThisArchive); + + var list = new ArrayList<>(removedFromThisArchive); + Collections.sort(list); + var sb = new StringBuilder("Removed ").append(list.get(0)); + for (int i = 1; i < list.size(); ++i) { + sb.append(",").append(list.get(i)); + } + appComponent.setPedigree(sb.toString()); } + manifestConfig.addComponent(appComponent); } } } @@ -1248,7 +1290,13 @@ static void filterJarFile(Path resolvedDep, Path targetPath, Set transfo } else { manifest = new Manifest(); } - try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath), manifest)) { + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath))) { + JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME); + // Set manifest time to epoch to always make the same jar + manifestEntry.setTime(0); + out.putNextEntry(manifestEntry); + manifest.write(out); + out.closeEntry(); Enumeration entries = in.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); @@ -1264,6 +1312,8 @@ static void filterJarFile(Path resolvedDep, Path targetPath, Set transfo while ((r = inStream.read(buffer)) > 0) { out.write(buffer, 0, r); } + } finally { + out.closeEntry(); } } else { log.debugf("Removed %s from %s", entryName, resolvedDep); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java index 6b5425c16078f1..9f04816dc62444 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java @@ -32,7 +32,7 @@ protected NativeImageBuildContainerRunner(NativeConfig nativeConfig) { this.baseContainerRuntimeArgs = new String[] { "--env", "LANG=C", "--rm" }; - containerName = "build-native-" + RandomStringUtils.random(5, true, false); + containerName = "build-native-" + RandomStringUtils.insecure().next(5, true, false); } @Override diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java index 1afb99e41f7fd9..c2ec3493c16949 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java @@ -11,40 +11,54 @@ import org.apache.commons.lang3.SystemUtils; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; import io.quarkus.deployment.util.FileUtil; public class NativeImageBuildLocalContainerRunner extends NativeImageBuildContainerRunner { public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig) { super(nativeConfig); + List containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs)); + if (SystemUtils.IS_OS_LINUX && containerRuntime.isInWindowsWSL()) { + containerRuntimeArgs.add("--interactive"); + } + containerRuntimeArgs.addAll(getVolumeAccessArguments(containerRuntime)); + baseContainerRuntimeArgs = containerRuntimeArgs.toArray(baseContainerRuntimeArgs); + } + + public static List getVolumeAccessArguments(ContainerRuntime containerRuntime) { + final List result = new ArrayList<>(); if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC) { - final ArrayList containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs)); - if (containerRuntime.isInWindowsWSL()) { - containerRuntimeArgs.add("--interactive"); - } if (containerRuntime.isDocker() && containerRuntime.isRootless()) { - Collections.addAll(containerRuntimeArgs, "--user", String.valueOf(0)); + Collections.addAll(result, "--user", String.valueOf(0)); } else { String uid = getLinuxID("-ur"); String gid = getLinuxID("-gr"); if (uid != null && gid != null && !uid.isEmpty() && !gid.isEmpty()) { - Collections.addAll(containerRuntimeArgs, "--user", uid + ":" + gid); + Collections.addAll(result, "--user", uid + ":" + gid); if (containerRuntime.isPodman() && containerRuntime.isRootless()) { // Needed to avoid AccessDeniedExceptions - containerRuntimeArgs.add("--userns=keep-id"); + result.add("--userns=keep-id"); } } } - baseContainerRuntimeArgs = containerRuntimeArgs.toArray(baseContainerRuntimeArgs); } + return result; } @Override protected List getContainerRuntimeBuildArgs(Path outputDir) { final List containerRuntimeArgs = super.getContainerRuntimeBuildArgs(outputDir); String volumeOutputPath = outputDir.toAbsolutePath().toString(); + addVolumeParameter(volumeOutputPath, NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH, containerRuntimeArgs, + containerRuntime); + return containerRuntimeArgs; + } + + public static void addVolumeParameter(String localPath, String remotePath, List args, + ContainerRuntime containerRuntime) { if (SystemUtils.IS_OS_WINDOWS) { - volumeOutputPath = FileUtil.translateToVolumePath(volumeOutputPath); + localPath = FileUtil.translateToVolumePath(localPath); } final String selinuxBindOption; @@ -54,9 +68,7 @@ protected List getContainerRuntimeBuildArgs(Path outputDir) { selinuxBindOption = ":z"; } - Collections.addAll(containerRuntimeArgs, "-v", - volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + selinuxBindOption); - return containerRuntimeArgs; + args.add("-v"); + args.add(localPath + ":" + remotePath + selinuxBindOption); } - } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java index cd433fb365c68a..94d402bb2d2116 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java @@ -3,6 +3,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; @@ -10,6 +11,11 @@ import org.jboss.logging.Logger; +import io.quarkus.builder.JsonReader; +import io.quarkus.builder.json.JsonArray; +import io.quarkus.builder.json.JsonObject; +import io.quarkus.builder.json.JsonString; +import io.quarkus.builder.json.JsonValue; import io.quarkus.deployment.pkg.NativeConfig; public class NativeImageBuildRemoteContainerRunner extends NativeImageBuildContainerRunner { @@ -34,22 +40,27 @@ protected void preBuild(Path outputDir, List buildArgs) throws Interrupt final List containerRuntimeArgs = Arrays.asList("-v", CONTAINER_BUILD_VOLUME_NAME + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH); final String[] createTempContainerCommand = buildCommand("create", containerRuntimeArgs, Collections.emptyList()); - containerId = runCommandAndReadOutput(createTempContainerCommand, "Failed to create temp container."); + try { + containerId = runCommandAndReadOutput(createTempContainerCommand).get(0); + } catch (RuntimeException | InterruptedException | IOException e) { + throw new RuntimeException("Failed to create temp container.", e); + } // docker cp :/project - String[] copyCommand = new String[] { containerRuntime.getExecutableName(), "cp", outputDir.toAbsolutePath() + "/.", + final String[] copyCommand = new String[] { + containerRuntime.getExecutableName(), "cp", outputDir.toAbsolutePath() + "/.", containerId + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH }; - runCommand(copyCommand, "Failed to copy source-jar and libs from host to builder container", null); + runCommand(copyCommand, "Failed to copy source-jar and libs from host to builder container"); super.preBuild(outputDir, buildArgs); } - private String runCommandAndReadOutput(String[] command, String errorMsg) throws IOException, InterruptedException { + private List runCommandAndReadOutput(String[] command) throws IOException, InterruptedException { log.info(String.join(" ", command).replace("$", "\\$")); - Process process = new ProcessBuilder(command).start(); + final Process process = new ProcessBuilder(command).start(); if (process.waitFor() != 0) { - throw new RuntimeException(errorMsg); + throw new RuntimeException("Command failed: " + String.join(" ", command)); } try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - return reader.readLine(); + return reader.lines().toList(); } } @@ -57,15 +68,45 @@ private String runCommandAndReadOutput(String[] command, String errorMsg) throws protected void postBuild(Path outputDir, String nativeImageName, String resultingExecutableName) { copyFromContainerVolume(outputDir, resultingExecutableName, "Failed to copy native image from container volume back to the host."); + + // Note that podman cp does not support globbing i.e. cp /project/*.so will not work. + // Why only .so? How about .dynlib and .lib? Regardless of the host platform, + // the builder container is always Linux. So, we only need to copy .so files. + // + // We could either start the container again, exec `find' or `ls' to list the .so files, + // stop the container again and use that list. We could also use the build-artifacts.json + // to get the list of artifacts straight away which is what ended up doing here: + copyFromContainerVolume(outputDir, "build-artifacts.json", null); + try { + final Path buildArtifactsFile = outputDir.resolve("build-artifacts.json"); + if (Files.exists(buildArtifactsFile)) { + // The file is small enough to afford this read + final String buildArtifactsJson = Files.readString(buildArtifactsFile); + final JsonObject jsonRead = JsonReader.of(buildArtifactsJson).read(); + final JsonValue jdkLibraries = jsonRead.get("jdk_libraries"); + // The jdk_libraries field is optional, there might not be any. + if (jdkLibraries instanceof JsonArray) { + for (JsonValue lib : ((JsonArray) jdkLibraries).value()) { + copyFromContainerVolume(outputDir, ((JsonString) lib).value(), + "Failed to copy " + lib + " from container volume back to the host."); + } + } + } + } catch (IOException e) { + log.errorf(e, "Failed to list .so files in the build-artifacts.json. Skipping the step."); + } + if (nativeConfig.debug().enabled()) { - copyFromContainerVolume(outputDir, "sources", "Failed to copy sources from container volume back to the host."); - String symbols = String.format("%s.debug", nativeImageName); - copyFromContainerVolume(outputDir, symbols, "Failed to copy debug symbols from container volume back to the host."); + copyFromContainerVolume(outputDir, "sources", + "Failed to copy sources from container volume back to the host."); + final String symbols = String.format("%s.debug", nativeImageName); + copyFromContainerVolume(outputDir, symbols, + "Failed to copy debug symbols from container volume back to the host."); } // docker container rm final String[] rmTempContainerCommand = new String[] { containerRuntime.getExecutableName(), "container", "rm", containerId }; - runCommand(rmTempContainerCommand, "Failed to remove container: " + containerId, null); + runCommand(rmTempContainerCommand, "Failed to remove container: " + containerId); // docker volume rm rmVolume("Failed to remove volume: " + CONTAINER_BUILD_VOLUME_NAME); } @@ -73,20 +114,20 @@ protected void postBuild(Path outputDir, String nativeImageName, String resultin private void rmVolume(String errorMsg) { final String[] rmVolumeCommand = new String[] { containerRuntime.getExecutableName(), "volume", "rm", CONTAINER_BUILD_VOLUME_NAME }; - runCommand(rmVolumeCommand, errorMsg, null); + runCommand(rmVolumeCommand, errorMsg); } private void copyFromContainerVolume(Path outputDir, String path, String errorMsg) { // docker cp :/project/ - String[] copyCommand = new String[] { containerRuntime.getExecutableName(), "cp", + final String[] copyCommand = new String[] { containerRuntime.getExecutableName(), "cp", containerId + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + "/" + path, outputDir.toAbsolutePath().toString() }; - runCommand(copyCommand, errorMsg, null); + runCommand(copyCommand, errorMsg); } @Override protected List getContainerRuntimeBuildArgs(Path outputDir) { - List containerRuntimeArgs = super.getContainerRuntimeBuildArgs(outputDir); + final List containerRuntimeArgs = super.getContainerRuntimeBuildArgs(outputDir); Collections.addAll(containerRuntimeArgs, "-v", CONTAINER_BUILD_VOLUME_NAME + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH); return containerRuntimeArgs; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java index 899cf9c280a01d..0b999594e66d33 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRunner.java @@ -11,6 +11,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import org.apache.commons.lang3.SystemUtils; import org.jboss.logging.Logger; @@ -132,22 +133,28 @@ static void runCommand(String[] command, String errorMsg, File workingDirectory) log.info(String.join(" ", command).replace("$", "\\$")); Process process = null; try { - final ProcessBuilder processBuilder = new ProcessBuilder(command); + final ProcessBuilder processBuilder = new ProcessBuilder(command) + .redirectErrorStream(true); if (workingDirectory != null) { processBuilder.directory(workingDirectory); } process = processBuilder.start(); final int exitCode = process.waitFor(); if (exitCode != 0) { + final String out; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + out = reader.lines().collect(Collectors.joining("\n")); + } if (errorMsg != null) { - log.error(errorMsg); + log.error(errorMsg + " Output: " + out); } else { - log.debugf("Command: " + String.join(" ", command) + " failed with exit code " + exitCode); + log.debugf( + "Command: " + String.join(" ", command) + " failed with exit code " + exitCode + " Output: " + out); } } } catch (IOException | InterruptedException e) { if (errorMsg != null) { - log.error(errorMsg); + log.errorf(e, errorMsg); } else { log.debugf(e, "Command: " + String.join(" ", command) + " failed."); } @@ -158,6 +165,16 @@ static void runCommand(String[] command, String errorMsg, File workingDirectory) } } + /** + * Run {@code command} and log error if {@code errorMsg} is not null. + * + * @param command + * @param errorMsg + */ + static void runCommand(String[] command, String errorMsg) { + runCommand(command, errorMsg, null); + } + static class Result { private final int exitCode; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index bf483fae4621fc..cb844cd4e5887a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -28,6 +28,7 @@ import io.quarkus.deployment.builditem.SuppressNonRuntimeConfigChangedWarningBuildItem; import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.JPMSExportBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAgentConfigDirectoryBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathAggregateBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageEnableModule; import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem; @@ -50,6 +51,8 @@ import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.runtime.LocalesBuildTimeConfig; import io.quarkus.runtime.graal.DisableLoggingFeature; +import io.quarkus.sbom.ApplicationComponent; +import io.quarkus.sbom.ApplicationManifestConfig; public class NativeImageBuildStep { @@ -85,10 +88,16 @@ void nativeImageFeatures(BuildProducer features) { } @BuildStep(onlyIf = NativeBuild.class) - ArtifactResultBuildItem result(NativeImageBuildItem image) { + ArtifactResultBuildItem result(NativeImageBuildItem image, + CurateOutcomeBuildItem curateOutcomeBuildItem) { NativeImageBuildItem.GraalVMVersion graalVMVersion = image.getGraalVMInfo(); return new ArtifactResultBuildItem(image.getPath(), "native", - graalVMVersion.toMap()); + graalVMVersion.toMap(), + ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setMainComponent(ApplicationComponent.builder().setPath(image.getPath())) + .setRunnerPath(image.getPath()) + .build()); } @BuildStep(onlyIf = NativeSourcesBuild.class) @@ -105,7 +114,8 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, List jpmsExportBuildItems, List nativeImageSecurityProviders, List nativeImageFeatures, - NativeImageRunnerBuildItem nativeImageRunner) { + NativeImageRunnerBuildItem nativeImageRunner, + CurateOutcomeBuildItem curateOutcomeBuildItem) { Path outputDir; try { @@ -158,7 +168,14 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, return new ArtifactResultBuildItem(nativeImageSourceJarBuildItem.getPath(), "native-sources", - Collections.emptyMap()); + Collections.emptyMap(), + ApplicationManifestConfig.builder() + .setApplicationModel(curateOutcomeBuildItem.getApplicationModel()) + .setMainComponent(ApplicationComponent.builder() + .setPath(nativeImageSourceJarBuildItem.getPath()) + .setResolvedDependency(curateOutcomeBuildItem.getApplicationModel().getAppArtifact())) + .setRunnerPath(nativeImageSourceJarBuildItem.getPath()) + .build()); } @BuildStep @@ -178,6 +195,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon Optional processInheritIODisabled, Optional processInheritIODisabledBuildItem, List nativeImageFeatures, + Optional nativeImageAgentConfigDirectoryBuildItem, NativeImageRunnerBuildItem nativeImageRunner) { if (nativeConfig.debug().enabled()) { copyJarSourcesToLib(outputTargetBuildItem, curateOutcomeBuildItem); @@ -207,7 +225,8 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon if (nativeConfig.reuseExisting()) { if (Files.exists(finalExecutablePath)) { return new NativeImageBuildItem(finalExecutablePath, - NativeImageBuildItem.GraalVMVersion.unknown()); + NativeImageBuildItem.GraalVMVersion.unknown(), + true); } } @@ -245,6 +264,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon .setGraalVMVersion(graalVMVersion) .setNativeImageFeatures(nativeImageFeatures) .setContainerBuild(isContainerBuild) + .setNativeImageAgentConfigDirectory(nativeImageAgentConfigDirectoryBuildItem) .build(); List nativeImageArgs = commandAndExecutable.args; @@ -294,7 +314,8 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon new NativeImageBuildItem.GraalVMVersion(graalVMVersion.fullVersion, graalVMVersion.getVersionAsString(), graalVMVersion.javaVersion.feature(), - graalVMVersion.distribution.name())); + graalVMVersion.distribution.name()), + false); } catch (ImageGenerationFailureException e) { throw e; } catch (Exception e) { @@ -334,7 +355,7 @@ public NativeImageRunnerBuildItem resolveNativeImageBuildRunner(NativeConfig nat } String executableName = getNativeImageExecutableName(); String errorMessage = "Cannot find the `" + executableName - + "` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image`"; + + "` in the GRAALVM_HOME, JAVA_HOME and System PATH."; if (!SystemUtils.IS_OS_LINUX) { // Delay the error: if we're just building native sources, we may not need the build runner at all. return new NativeImageRunnerBuildItem(new NativeImageBuildRunnerError(errorMessage)); @@ -593,12 +614,19 @@ static class Builder { private String nativeImageName; private boolean classpathIsBroken; private boolean containerBuild; + private Optional nativeImageAgentConfigDirectory = Optional.empty(); public Builder setNativeConfig(NativeConfig nativeConfig) { this.nativeConfig = nativeConfig; return this; } + public Builder setNativeImageAgentConfigDirectory( + Optional nativeImageAgentConfigDirectory) { + this.nativeImageAgentConfigDirectory = nativeImageAgentConfigDirectory; + return this; + } + public Builder setLocalesBuildTimeConfig(LocalesBuildTimeConfig localesBuildTimeConfig) { this.localesBuildTimeConfig = localesBuildTimeConfig; return this; @@ -778,6 +806,7 @@ public NativeImageInvokerInfo build() { * control its actual inclusion which will depend on the usual analysis. */ nativeImageArgs.add("-J--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED"); + nativeImageArgs.add("-J--add-exports=java.security.jgss/sun.security.jgss=ALL-UNNAMED"); //address https://github.com/quarkusio/quarkus-quickstarts/issues/993 nativeImageArgs.add("-J--add-opens=java.base/java.text=ALL-UNNAMED"); @@ -805,6 +834,11 @@ public NativeImageInvokerInfo build() { "-H:BuildOutputJSONFile=" + nativeImageName + "-build-output-stats.json"); } + // only available in GraalVM 23.0+, we want a file with the list of built artifacts + if (graalVMVersion.compareTo(GraalVM.Version.VERSION_23_0_0) >= 0) { + addExperimentalVMOption(nativeImageArgs, "-H:+GenerateBuildArtifactsFile"); + } + // only available in GraalVM 23.1.0+ if (graalVMVersion.compareTo(GraalVM.Version.VERSION_23_1_0) >= 0) { if (graalVMVersion.compareTo(GraalVM.Version.VERSION_24_0_0) < 0) { @@ -983,6 +1017,9 @@ public NativeImageInvokerInfo build() { } } + nativeImageAgentConfigDirectory + .ifPresent(dir -> nativeImageArgs.add("-H:ConfigurationFileDirectories=" + dir.getDirectory())); + for (ExcludeConfigBuildItem excludeConfig : excludeConfigs) { nativeImageArgs.add("--exclude-config"); nativeImageArgs.add(excludeConfig.getJarFile()); 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 18807695b95290..8e2ca4272772b3 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 @@ -40,10 +40,15 @@ public void compress(NativeConfig nativeConfig, NativeImageRunnerBuildItem nativ NativeImageBuildItem image, BuildProducer upxCompressedProducer, BuildProducer artifactResultProducer) { + if (nativeConfig.compression().level().isEmpty()) { log.debug("UPX compression disabled"); return; } + if (image.isReused()) { + log.debug("Native executable reused: skipping compression"); + return; + } String effectiveBuilderImage = nativeConfig.builderImage().getEffectiveImage(); Optional upxPathFromSystem = getUpxFromSystem(); @@ -53,16 +58,16 @@ public void compress(NativeConfig nativeConfig, NativeImageRunnerBuildItem nativ throw new IllegalStateException("Unable to compress the native executable"); } } else if (nativeConfig.remoteContainerBuild()) { - log.errorf("Compression of native executables is not yet implemented for remote container builds."); + log.error("Compression of native executables is not yet implemented for remote container builds."); throw new IllegalStateException( "Unable to compress the native executable: Compression of native executables is not yet supported for remote container builds"); } else if (nativeImageRunner.isContainerBuild()) { - log.infof("Running UPX from a container using the builder image: " + effectiveBuilderImage); + log.info("Running UPX from a container using the builder image: " + effectiveBuilderImage); if (!runUpxInContainer(image, nativeConfig, effectiveBuilderImage)) { throw new IllegalStateException("Unable to compress the native executable"); } } else { - log.errorf("Unable to compress the native executable. Either install `upx` from https://upx.github.io/" + + log.error("Unable to compress the native executable. Either install `upx` from https://upx.github.io/" + " on your machine, or enable in-container build using `-Dquarkus.native.container-build=true`."); throw new IllegalStateException("Unable to compress the native executable: `upx` not available"); } @@ -90,12 +95,12 @@ private boolean runUpxFromHost(File upx, File executable, NativeConfig nativeCon ProcessUtil.streamOutputToSysOut(process); final int exitCode = process.waitFor(); if (exitCode != 0) { - log.errorf("Command: " + String.join(" ", args) + " failed with exit code " + exitCode); + log.error("Command: " + String.join(" ", args) + " failed with exit code " + exitCode); return false; } return true; } catch (Exception e) { - log.errorf("Command: " + String.join(" ", args) + " failed", e); + log.error("Command: " + String.join(" ", args) + " failed", e); return false; } finally { if (process != null) { @@ -119,7 +124,7 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig commandLine.add("--rm"); commandLine.add("--entrypoint=upx"); - String containerName = "upx-" + RandomStringUtils.random(5, true, false); + String containerName = "upx-" + RandomStringUtils.insecure().next(5, true, false); commandLine.add("--name"); commandLine.add(containerName); @@ -169,7 +174,7 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig } return true; } catch (Exception e) { - log.errorf("Command: " + String.join(" ", commandLine) + " failed", e); + log.error("Command: " + String.join(" ", commandLine) + " failed", e); return false; } finally { if (process != null) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java index 651d21b79eddc5..23ff1bda6ba402 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java @@ -8,12 +8,14 @@ import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; +import java.lang.reflect.WildcardType; import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; @@ -71,6 +73,9 @@ import io.quarkus.runtime.StartupTask; import io.quarkus.runtime.annotations.IgnoreProperty; import io.quarkus.runtime.annotations.RelaxedValidation; +import io.quarkus.runtime.types.GenericArrayTypeImpl; +import io.quarkus.runtime.types.ParameterizedTypeImpl; +import io.quarkus.runtime.types.WildcardTypeImpl; /** * A class that can be used to record invocations to bytecode so they can be replayed later. This is done through the @@ -769,6 +774,68 @@ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle ar } }; } + } else if (param instanceof ParameterizedType parameterized) { + DeferredParameter raw = loadObjectInstance(parameterized.getRawType(), existing, + java.lang.reflect.Type.class, relaxedValidation); + DeferredParameter args = loadObjectInstance(parameterized.getActualTypeArguments(), existing, + java.lang.reflect.Type[].class, relaxedValidation); + DeferredParameter owner = loadObjectInstance(parameterized.getOwnerType(), existing, + java.lang.reflect.Type.class, relaxedValidation); + return new DeferredParameter() { + @Override + ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) { + return method.newInstance(ofConstructor(ParameterizedTypeImpl.class, java.lang.reflect.Type.class, + java.lang.reflect.Type[].class, java.lang.reflect.Type.class), + context.loadDeferred(raw), context.loadDeferred(args), context.loadDeferred(owner)); + } + }; + } else if (param instanceof GenericArrayType array) { + DeferredParameter res = loadObjectInstance(array.getGenericComponentType(), existing, + java.lang.reflect.Type.class, relaxedValidation); + return new DeferredParameter() { + @Override + ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) { + return method.newInstance(ofConstructor(GenericArrayTypeImpl.class, java.lang.reflect.Type.class), + context.loadDeferred(res)); + } + }; + } else if (param instanceof WildcardType wildcard) { + java.lang.reflect.Type[] upperBound = wildcard.getUpperBounds(); + java.lang.reflect.Type[] lowerBound = wildcard.getLowerBounds(); + if (lowerBound.length == 0 && upperBound.length == 1 && Object.class.equals(upperBound[0])) { + // unbounded + return new DeferredParameter() { + @Override + ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) { + return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "defaultInstance", + WildcardType.class)); + } + }; + } else if (lowerBound.length == 0 && upperBound.length == 1) { + // upper bound + DeferredParameter res = loadObjectInstance(upperBound[0], existing, + java.lang.reflect.Type.class, relaxedValidation); + return new DeferredParameter() { + @Override + ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) { + return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "withUpperBound", + WildcardType.class, java.lang.reflect.Type.class), context.loadDeferred(res)); + } + }; + } else if (lowerBound.length == 1) { + // lower bound + DeferredParameter res = loadObjectInstance(lowerBound[0], existing, + java.lang.reflect.Type.class, relaxedValidation); + return new DeferredParameter() { + @Override + ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) { + return method.invokeStaticMethod(ofMethod(WildcardTypeImpl.class, "withLowerBound", + WildcardType.class, java.lang.reflect.Type.class), context.loadDeferred(res)); + } + }; + } else { + throw new UnsupportedOperationException("Unsupported wildcard type: " + wildcard); + } } else if (expectedType == boolean.class || expectedType == Boolean.class || param instanceof Boolean) { return new DeferredParameter() { @Override @@ -1135,15 +1202,7 @@ public void prepare(MethodContext context) { nonDefaultConstructorHandles[i] = loadObjectInstance(obj, existing, parameterTypes[count++], relaxedValidation); } - if (nonDefaultConstructorHolder.constructor.getParameterCount() > 0) { - Parameter[] parameters = nonDefaultConstructorHolder.constructor.getParameters(); - for (int i = 0; i < parameters.length; ++i) { - if (parameters[i].isNamePresent()) { - String name = parameters[i].getName(); - constructorParamNameMap.put(name, i); - } - } - } + extractConstructorParameterNames(nonDefaultConstructorHolder.constructor, constructorParamNameMap); } else if (classesToUseRecordableConstructor.contains(param.getClass())) { Constructor current = null; int count = 0; @@ -1151,24 +1210,17 @@ public void prepare(MethodContext context) { if (current == null || current.getParameterCount() < c.getParameterCount()) { current = c; count = 0; - } else if (current != null && current.getParameterCount() == c.getParameterCount()) { + } else if (current.getParameterCount() == c.getParameterCount()) { count++; } } if (current == null || count > 0) { throw new RuntimeException("Unable to determine the recordable constructor to use for " + param.getClass()); } + nonDefaultConstructorHolder = new NonDefaultConstructorHolder(current, null); nonDefaultConstructorHandles = new DeferredParameter[current.getParameterCount()]; - if (current.getParameterCount() > 0) { - Parameter[] parameters = current.getParameters(); - for (int i = 0; i < parameters.length; ++i) { - if (parameters[i].isNamePresent()) { - String name = parameters[i].getName(); - constructorParamNameMap.put(name, i); - } - } - } + extractConstructorParameterNames(current, constructorParamNameMap); } else { Constructor[] ctors = param.getClass().getConstructors(); Constructor selectedCtor = null; @@ -1184,16 +1236,13 @@ public void prepare(MethodContext context) { } if (selectedCtor != null) { nonDefaultConstructorHolder = new NonDefaultConstructorHolder(selectedCtor, null); - nonDefaultConstructorHandles = new DeferredParameter[selectedCtor.getParameterCount()]; - - if (selectedCtor.getParameterCount() > 0) { - Parameter[] ctorParameters = selectedCtor.getParameters(); - for (int i = 0; i < ctorParameters.length; ++i) { - if (ctorParameters[i].isNamePresent()) { - String name = ctorParameters[i].getName(); - constructorParamNameMap.put(name, i); - } - } + final var parameterCount = selectedCtor.getParameterCount(); + nonDefaultConstructorHandles = new DeferredParameter[parameterCount]; + extractConstructorParameterNames(selectedCtor, constructorParamNameMap); + + if (constructorParamNameMap.size() != parameterCount) { + throw new IllegalArgumentException("Couldn't extract all parameters information for constructor " + + selectedCtor + " for type " + expectedType); } } } @@ -1235,9 +1284,16 @@ public void prepare(MethodContext context) { public void handle(MethodContext context, MethodCreator method, DeferredArrayStoreParameter out) { //get the collection - ResultHandle prop = method.invokeVirtualMethod( - MethodDescriptor.ofMethod(i.getReadMethod()), - context.loadDeferred(out)); + ResultHandle prop; + if (i.getReadMethod().isDefault()) { + prop = method.invokeInterfaceMethod( + MethodDescriptor.ofMethod(i.getReadMethod()), + context.loadDeferred(out)); + } else { + prop = method.invokeVirtualMethod( + MethodDescriptor.ofMethod(i.getReadMethod()), + context.loadDeferred(out)); + } for (DeferredParameter i : params) { //add the parameter //TODO: this is not guarded against large collections, probably not an issue in practice @@ -1351,8 +1407,15 @@ public void prepare(MethodContext context) { } } } - DeferredParameter val = loadObjectInstance(propertyValue, existing, - i.getPropertyType(), relaxedValidation); + DeferredParameter val; + try { + val = loadObjectInstance(propertyValue, existing, + i.getPropertyType(), relaxedValidation); + } catch (Exception e) { + throw new RuntimeException( + "Couldn't load object of type " + i.propertyType.getName() + " for property '" + i.getName() + + "' on object '" + param + "'."); + } if (ctorParamIndex != null) { nonDefaultConstructorHandles[ctorParamIndex] = val; ctorSetupSteps.add(new SerializationStep() { @@ -1451,7 +1514,7 @@ public void prepare(MethodContext context) { NonDefaultConstructorHolder finalNonDefaultConstructorHolder = nonDefaultConstructorHolder; DeferredParameter[] finalCtorHandles = nonDefaultConstructorHandles; - //create a deferred value to represet the object itself. This allows the creation to be split + //create a deferred value to represent the object itself. This allows the creation to be split //over multiple methods, which is important if this is a large object DeferredArrayStoreParameter objectValue = new DeferredArrayStoreParameter(param, expectedType) { @Override @@ -1533,6 +1596,24 @@ ResultHandle createValue(MethodContext context, MethodCreator method, ResultHand }; } + private static List extractConstructorParameterNames(Constructor selectedCtor, + Map constructorParamNameMap) { + List unnamed = Collections.emptyList(); + if (selectedCtor.getParameterCount() > 0) { + Parameter[] ctorParameters = selectedCtor.getParameters(); + unnamed = new ArrayList<>(ctorParameters.length); + for (int i = 0; i < ctorParameters.length; ++i) { + if (ctorParameters[i].isNamePresent()) { + String name = ctorParameters[i].getName(); + constructorParamNameMap.put(name, i); + } else { + unnamed.add(ctorParameters[i]); + } + } + } + return unnamed; + } + /** * Returns {@code true} iff the field is annotated {@link IgnoreProperty} or the field is marked as {@code transient} */ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/sbom/SbomBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/sbom/SbomBuildItem.java new file mode 100644 index 00000000000000..03a6136ab5ecfc --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/sbom/SbomBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.deployment.sbom; + +import java.util.Objects; + +import io.quarkus.bootstrap.app.SbomResult; +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Aggregates SBOMs generated for packaged applications. + * The API around this is still in development and will likely change in the near future. + */ +public final class SbomBuildItem extends MultiBuildItem { + + private final SbomResult result; + + public SbomBuildItem(SbomResult result) { + this.result = Objects.requireNonNull(result); + } + + public SbomResult getResult() { + return result; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/shutdown/ShutdownBuildTimeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/shutdown/ShutdownBuildTimeConfig.java index 425b88aa59dd38..1f86a511c190c4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/shutdown/ShutdownBuildTimeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/shutdown/ShutdownBuildTimeConfig.java @@ -4,6 +4,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Shutdown + */ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class ShutdownBuildTimeConfig { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/AdditionalClassLoaderResourcesBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/AdditionalClassLoaderResourcesBuildStep.java index 2b1a8fbdf8d5ca..9eb2aacd91fe26 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/AdditionalClassLoaderResourcesBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/AdditionalClassLoaderResourcesBuildStep.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.steps; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.util.ArrayList; import java.util.Collections; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ApplyNativeImageAgentConfigStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ApplyNativeImageAgentConfigStep.java new file mode 100644 index 00000000000000..ca68bac029d2f0 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ApplyNativeImageAgentConfigStep.java @@ -0,0 +1,58 @@ +package io.quarkus.deployment.steps; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import org.jboss.logging.Logger; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAgentConfigDirectoryBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.NativeImageSourceJarBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; + +/** + * This configuration step looks for native configuration folder generated + * with the native image agent running inside Quarkus integration tests. + * If the folder is detected and {@link NativeConfig#agentConfigurationApply()} is enabled, + * the folder's path is passed onto the {@link io.quarkus.deployment.pkg.steps.NativeImageBuildStep}, + * wrapped inside a {@link NativeImageAgentConfigDirectoryBuildItem}, + * so that the folder is added as a configuration folder for the native image process execution. + */ +public class ApplyNativeImageAgentConfigStep { + private static final Logger log = Logger.getLogger(ApplyNativeImageAgentConfigStep.class); + + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + void transformConfig(NativeConfig nativeConfig, + BuildProducer nativeImageAgentConfigDirectoryProducer, + NativeImageSourceJarBuildItem nativeImageSourceJarBuildItem, + BuildSystemTargetBuildItem buildSystemTargetBuildItem) throws IOException { + final Path basePath = buildSystemTargetBuildItem.getOutputDirectory() + .resolve(Path.of("native-image-agent-final-config")); + if (basePath.toFile().exists() && nativeConfig.agentConfigurationApply()) { + final Path outputDir = nativeImageSourceJarBuildItem.getPath().getParent(); + final String targetDirName = "native-image-agent-config"; + final Path targetPath = outputDir.resolve(Path.of(targetDirName)); + if (!targetPath.toFile().exists()) { + targetPath.toFile().mkdirs(); + } + Files.copy(basePath.resolve("reflect-config.json"), targetPath.resolve("reflect-config.json"), + StandardCopyOption.REPLACE_EXISTING); + Files.copy(basePath.resolve("serialization-config.json"), targetPath.resolve("serialization-config.json"), + StandardCopyOption.REPLACE_EXISTING); + Files.copy(basePath.resolve("jni-config.json"), targetPath.resolve("jni-config.json"), + StandardCopyOption.REPLACE_EXISTING); + Files.copy(basePath.resolve("proxy-config.json"), targetPath.resolve("proxy-config.json"), + StandardCopyOption.REPLACE_EXISTING); + Files.copy(basePath.resolve("resource-config.json"), targetPath.resolve("resource-config.json"), + StandardCopyOption.REPLACE_EXISTING); + + log.info("Applying native image agent generated files to current native executable build"); + nativeImageAgentConfigDirectoryProducer.produce(new NativeImageAgentConfigDirectoryBuildItem(targetDirName)); + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/BannerProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/BannerProcessor.java index b881235bd7375d..bce66170f08006 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/BannerProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/BannerProcessor.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.steps; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java index d318e12f5a584a..921cb5fc8993ea 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.steps; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.File; import java.io.FileOutputStream; @@ -369,8 +369,9 @@ private byte[] transformClass(String className, List majorVersion = new AtomicReference<>(null); - try { - log.debugf("Walking directory '%s'", buildSystemTarget.getOutputDirectory().toAbsolutePath().toString()); - Files.walkFileTree(buildSystemTarget.getOutputDirectory(), new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (file.getFileName().toString().endsWith(".class")) { - log.debugf("Checking file '%s'", file.toAbsolutePath().toString()); - try (InputStream in = new FileInputStream(file.toFile())) { - DataInputStream data = new DataInputStream(in); - if (0xCAFEBABE == data.readInt()) { - data.readUnsignedShort(); // minor version -> we don't care about it - int v = data.readUnsignedShort(); - majorVersion.set(v); - log.debugf("Determined compile java version to be %d", v); - return FileVisitResult.TERMINATE; - } - } catch (IOException e) { - log.debugf(e, "Encountered exception while processing file '%s'", file.toAbsolutePath().toString()); - } - } - // if this was not .class file or there was an error parsing its contents, we continue on to the next file - return FileVisitResult.CONTINUE; + public CompiledJavaVersionBuildItem compiledJavaVersion(CurateOutcomeBuildItem curateOutcomeBuildItem) { + final ResolvedDependency appArtifact = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); + Integer majorVersion = getMajorJavaVersion(appArtifact); + if (majorVersion == null) { + // workspace info isn't available in prod builds though + for (ResolvedDependency module : curateOutcomeBuildItem.getApplicationModel() + .getDependencies(DependencyFlags.WORKSPACE_MODULE)) { + majorVersion = getMajorJavaVersion(module); + if (majorVersion != null) { + break; } - }); - } catch (IOException ignored) { - + } } - if (majorVersion.get() == null) { + if (majorVersion == null) { log.debug("No .class files located"); return CompiledJavaVersionBuildItem.unknown(); } - return CompiledJavaVersionBuildItem.fromMajorJavaVersion(majorVersion.get()); + return CompiledJavaVersionBuildItem.fromMajorJavaVersion(majorVersion); + } + + private static Integer getMajorJavaVersion(ResolvedDependency artifact) { + final AtomicReference majorVersion = new AtomicReference<>(null); + artifact.getContentTree().walk(visit -> { + final Path file = visit.getPath(); + if (file.getFileName() == null) { + // this can happen if it's the root of a JAR + return; + } + if (file.getFileName().toString().endsWith(".class") && !Files.isDirectory(file)) { + log.debugf("Checking file '%s'", file.toAbsolutePath().toString()); + try (DataInputStream data = new DataInputStream(Files.newInputStream(file))) { + if (0xCAFEBABE == data.readInt()) { + data.readUnsignedShort(); // minor version -> we don't care about it + int v = data.readUnsignedShort(); + majorVersion.set(v); + log.debugf("Determined compile java version to be %d", v); + visit.stopWalking(); + } + } catch (IOException e) { + log.debugf(e, "Encountered exception while processing file '%s'", file.toAbsolutePath().toString()); + } + } + }); + return majorVersion.get(); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index feb22a65981b2f..134e3c0af1a10a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -157,7 +157,7 @@ void buildTimeRunTimeConfig( method.returnValue(configBuilder); } - reflectiveClass.produce(ReflectiveClassBuildItem.builder(builderClassName).build()); + reflectiveClass.produce(ReflectiveClassBuildItem.builder(builderClassName).reason(getClass().getName()).build()); staticInitConfigBuilder.produce(new StaticInitConfigBuilderBuildItem(builderClassName)); runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(builderClassName)); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index 9274c1ced43dfb..9b5a17a3808536 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -57,6 +57,7 @@ import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem; import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; import io.quarkus.deployment.naming.NamingConfig; import io.quarkus.deployment.pkg.PackageConfig; @@ -96,6 +97,9 @@ public class MainClassBuildStep { static final String STARTUP_CONTEXT = "STARTUP_CONTEXT"; static final String LOG = "LOG"; static final String JAVA_LIBRARY_PATH = "java.library.path"; + // This is declared as a constant so that it can be grepped for in the native-image binary using `strings`, e.g.: + // strings ./target/quarkus-runner | grep "__quarkus_analytics__quarkus.version=" + public static final String QUARKUS_ANALYTICS_QUARKUS_VERSION = "__QUARKUS_ANALYTICS_QUARKUS_VERSION"; public static final String GENERATE_APP_CDS_SYSTEM_PROPERTY = "quarkus.appcds.generate"; @@ -155,6 +159,9 @@ void build(List staticInitTasks, FieldCreator scField = file.getFieldCreator(STARTUP_CONTEXT_FIELD); scField.setModifiers(Modifier.PUBLIC | Modifier.STATIC); + FieldCreator quarkusVersionField = file.getFieldCreator(QUARKUS_ANALYTICS_QUARKUS_VERSION, String.class) + .setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL); + MethodCreator ctor = file.getMethodCreator("", void.class); ctor.invokeSpecialMethod(ofMethod(Application.class, "", void.class, boolean.class), ctor.getThis(), ctor.load(launchMode.isAuxiliaryApplication())); @@ -192,6 +199,10 @@ void build(List staticInitTasks, mv.writeStaticField(logField.getFieldDescriptor(), mv.invokeStaticMethod( ofMethod(Logger.class, "getLogger", Logger.class, String.class), mv.load("io.quarkus.application"))); + // Init the __QUARKUS_ANALYTICS_QUARKUS_VERSION field + mv.writeStaticField(quarkusVersionField.getFieldDescriptor(), + mv.load("__quarkus_analytics__quarkus.version=" + Version.getVersion())); + ResultHandle startupContext = mv.newInstance(ofConstructor(StartupContext.class)); mv.writeStaticField(scField.getFieldDescriptor(), startupContext); TryBlock tryBlock = mv.tryBlock(); @@ -512,7 +523,7 @@ private void writeRecordedBytecode(BytecodeRecorderImpl recorder, String fallbac */ @BuildStep ReflectiveClassBuildItem applicationReflection() { - return ReflectiveClassBuildItem.builder(Application.APP_CLASS_NAME).build(); + return ReflectiveClassBuildItem.builder(Application.APP_CLASS_NAME).reason("The generated application class").build(); } /** @@ -703,4 +714,10 @@ private static Result invalid() { } } + @BuildStep + ReflectiveFieldBuildItem setupVersionField() { + return new ReflectiveFieldBuildItem( + "Ensure it's included in the executable to be able to grep the quarkus version", + Application.APP_CLASS_NAME, QUARKUS_ANALYTICS_QUARKUS_VERSION); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java index 2ac0440ee5137b..a7507d05384680 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java @@ -102,6 +102,7 @@ void reinitHostNameUtil(BuildProducer runtim // so we reinitialize this to re-compute the field (and other related fields) during native application's // runtime runtimeReInitClass.produce(new RuntimeReinitializedClassBuildItem("org.wildfly.common.net.HostName")); + runtimeReInitClass.produce(new RuntimeReinitializedClassBuildItem("io.smallrye.common.net.HostName")); } private Boolean isSslNativeEnabled(SslNativeConfigBuildItem sslNativeConfig, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageReflectConfigStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageReflectConfigStep.java index cd04106f20330f..efe9a04ac4ddde 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageReflectConfigStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageReflectConfigStep.java @@ -21,12 +21,14 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; public class NativeImageReflectConfigStep { @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) void generateReflectConfig(BuildProducer reflectConfig, + NativeConfig nativeConfig, List reflectiveMethods, List reflectiveFields, List reflectiveClassBuildItems, @@ -40,8 +42,7 @@ void generateReflectConfig(BuildProducer reflectConf forcedNonWeakClasses.add(nonWeakReflectiveClassBuildItem.getClassName()); } for (ReflectiveClassBuildItem i : reflectiveClassBuildItems) { - addReflectiveClass(reflectiveClasses, forcedNonWeakClasses, i.isConstructors(), i.isMethods(), i.isFields(), - i.isWeak(), i.isSerialization(), i.isUnsafeAllocated(), i.getClassNames().toArray(new String[0])); + addReflectiveClass(reflectiveClasses, forcedNonWeakClasses, i); } for (ReflectiveFieldBuildItem i : reflectiveFields) { addReflectiveField(reflectiveClasses, i); @@ -51,8 +52,16 @@ void generateReflectConfig(BuildProducer reflectConf } for (ServiceProviderBuildItem i : serviceProviderBuildItems) { - addReflectiveClass(reflectiveClasses, forcedNonWeakClasses, true, false, false, false, false, false, - i.providers().toArray(new String[] {})); + for (String provider : i.providers()) { + // Register the nullary constructor + addReflectiveMethod(reflectiveClasses, + new ReflectiveMethodBuildItem("Class registered as provider", provider, "", new String[0])); + // Register public provider() method for lookkup to avoid throwing a MissingReflectionRegistrationError at run time. + // See ServiceLoader#loadProvider and ServiceLoader#findStaticProviderMethod. + addReflectiveMethod(reflectiveClasses, + new ReflectiveMethodBuildItem("Class registered as provider", true, provider, "provider", + new String[0])); + } } // Perform this as last step, since it augments the already added reflective classes @@ -71,40 +80,39 @@ void generateReflectConfig(BuildProducer reflectConf ReflectionInfo info = entry.getValue(); JsonArrayBuilder methodsArray = Json.array(); + JsonArrayBuilder queriedMethodsArray = Json.array(); if (info.typeReachable != null) { json.put("condition", Json.object().put("typeReachable", info.typeReachable)); } if (info.constructors) { json.put("allDeclaredConstructors", true); - } else if (!info.ctorSet.isEmpty()) { - for (ReflectiveMethodBuildItem ctor : info.ctorSet) { - JsonObjectBuilder methodObject = Json.object(); - methodObject.put("name", ctor.getName()); - JsonArrayBuilder paramsArray = Json.array(); - for (int i = 0; i < ctor.getParams().length; ++i) { - paramsArray.add(ctor.getParams()[i]); - } - methodObject.put("parameterTypes", paramsArray); - methodsArray.add(methodObject); + } else { + if (info.queryConstructors) { + json.put("queryAllDeclaredConstructors", true); + } + if (!info.ctorSet.isEmpty()) { + extractToJsonArray(info.ctorSet, methodsArray); } } if (info.methods) { json.put("allDeclaredMethods", true); - } else if (!info.methodSet.isEmpty()) { - for (ReflectiveMethodBuildItem method : info.methodSet) { - JsonObjectBuilder methodObject = Json.object(); - methodObject.put("name", method.getName()); - JsonArrayBuilder paramsArray = Json.array(); - for (int i = 0; i < method.getParams().length; ++i) { - paramsArray.add(method.getParams()[i]); - } - methodObject.put("parameterTypes", paramsArray); - methodsArray.add(methodObject); + } else { + if (info.queryMethods) { + json.put("queryAllDeclaredMethods", true); + } + if (!info.methodSet.isEmpty()) { + extractToJsonArray(info.methodSet, methodsArray); + } + if (!info.queriedMethodSet.isEmpty()) { + extractToJsonArray(info.queriedMethodSet, queriedMethodsArray); } } if (!methodsArray.isEmpty()) { json.put("methods", methodsArray); } + if (!queriedMethodsArray.isEmpty()) { + json.put("queriedMethods", queriedMethodsArray); + } if (info.fields) { json.put("allDeclaredFields", true); @@ -115,9 +123,19 @@ void generateReflectConfig(BuildProducer reflectConf } json.put("fields", fieldsArray); } + if (info.classes) { + json.put("allDeclaredClasses", true); + } if (info.unsafeAllocated) { json.put("unsafeAllocated", true); } + if (nativeConfig.includeReasonsInConfigFiles() && info.reasons != null) { + JsonArrayBuilder reasonsArray = Json.array(); + for (String reason : info.reasons) { + reasonsArray.add(reason); + } + json.put("reasons", reasonsArray); + } root.add(json); } @@ -131,6 +149,19 @@ void generateReflectConfig(BuildProducer reflectConf } } + private static void extractToJsonArray(Set methodSet, JsonArrayBuilder methodsArray) { + for (ReflectiveMethodBuildItem method : methodSet) { + JsonObjectBuilder methodObject = Json.object(); + methodObject.put("name", method.getName()); + JsonArrayBuilder paramsArray = Json.array(); + for (int i = 0; i < method.getParams().length; ++i) { + paramsArray.add(method.getParams()[i]); + } + methodObject.put("parameterTypes", paramsArray); + methodsArray.add(methodObject); + } + } + public void addReflectiveMethod(Map reflectiveClasses, ReflectiveMethodBuildItem methodInfo) { String cl = methodInfo.getDeclaringClass(); ReflectionInfo existing = reflectiveClasses.get(cl); @@ -140,36 +171,59 @@ public void addReflectiveMethod(Map reflectiveClasses, R if (methodInfo.getName().equals("")) { existing.ctorSet.add(methodInfo); } else { - existing.methodSet.add(methodInfo); + if (methodInfo.isQueryOnly()) { + existing.queriedMethodSet.add(methodInfo); + } else { + existing.methodSet.add(methodInfo); + } + } + String reason = methodInfo.getReason(); + if (reason != null) { + if (existing.reasons == null) { + existing.reasons = new HashSet<>(); + } + existing.reasons.add(reason); } } public void addReflectiveClass(Map reflectiveClasses, Set forcedNonWeakClasses, - boolean constructors, boolean method, - boolean fields, boolean weak, boolean serialization, boolean unsafeAllocated, - String... className) { - for (String cl : className) { + ReflectiveClassBuildItem classBuildItem) { + for (String cl : classBuildItem.getClassNames()) { ReflectionInfo existing = reflectiveClasses.get(cl); if (existing == null) { - String typeReachable = (!forcedNonWeakClasses.contains(cl) && weak) ? cl : null; - reflectiveClasses.put(cl, new ReflectionInfo(constructors, method, fields, - typeReachable, serialization, unsafeAllocated)); + String typeReachable = (!forcedNonWeakClasses.contains(cl) && classBuildItem.isWeak()) ? cl : null; + reflectiveClasses.put(cl, new ReflectionInfo(classBuildItem, typeReachable)); } else { - if (constructors) { + if (classBuildItem.isConstructors()) { existing.constructors = true; } - if (method) { + if (classBuildItem.isQueryConstructors()) { + existing.queryConstructors = true; + } + if (classBuildItem.isMethods()) { existing.methods = true; } - if (fields) { + if (classBuildItem.isQueryMethods()) { + existing.queryMethods = true; + } + if (classBuildItem.isFields()) { existing.fields = true; } - if (serialization) { + if (classBuildItem.isClasses()) { + existing.classes = true; + } + if (classBuildItem.isSerialization()) { existing.serialization = true; } - if (unsafeAllocated) { + if (classBuildItem.isUnsafeAllocated()) { existing.unsafeAllocated = true; } + if (classBuildItem.getReason() != null) { + if (existing.reasons == null) { + existing.reasons = new HashSet<>(); + } + existing.reasons.add(classBuildItem.getReason()); + } } } } @@ -181,31 +235,48 @@ public void addReflectiveField(Map reflectiveClasses, Re reflectiveClasses.put(cl, existing = new ReflectionInfo()); } existing.fieldSet.add(fieldInfo.getName()); + String reason = fieldInfo.getReason(); + if (reason != null) { + if (existing.reasons == null) { + existing.reasons = new HashSet<>(); + } + existing.reasons.add(reason); + } } static final class ReflectionInfo { boolean constructors; + boolean queryConstructors; boolean methods; + boolean queryMethods; boolean fields; + boolean classes; boolean serialization; boolean unsafeAllocated; + Set reasons = null; String typeReachable; Set fieldSet = new HashSet<>(); Set methodSet = new HashSet<>(); + Set queriedMethodSet = new HashSet<>(); Set ctorSet = new HashSet<>(); private ReflectionInfo() { - this(false, false, false, null, false, false); } - private ReflectionInfo(boolean constructors, boolean methods, boolean fields, String typeReachable, - boolean serialization, boolean unsafeAllocated) { - this.methods = methods; - this.fields = fields; + private ReflectionInfo(ReflectiveClassBuildItem classBuildItem, String typeReachable) { + this.methods = classBuildItem.isMethods(); + this.queryMethods = classBuildItem.isQueryMethods(); + this.fields = classBuildItem.isFields(); + this.classes = classBuildItem.isClasses(); this.typeReachable = typeReachable; - this.constructors = constructors; - this.serialization = serialization; - this.unsafeAllocated = unsafeAllocated; + this.constructors = classBuildItem.isConstructors(); + this.queryConstructors = classBuildItem.isQueryConstructors(); + this.serialization = classBuildItem.isSerialization(); + this.unsafeAllocated = classBuildItem.isUnsafeAllocated(); + if (classBuildItem.getReason() != null) { + reasons = new HashSet<>(); + reasons.add(classBuildItem.getReason()); + } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java index d63b5535faf49e..08e83716a7ff52 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java @@ -136,6 +136,7 @@ private void addReflectiveHierarchy(CombinedIndexBuildItem combinedIndexBuildIte Set processedReflectiveHierarchies, Map> unindexedClasses, Predicate finalFieldsWritable, BuildProducer reflectiveClass, Deque visits) { + final String newSource = source + " > " + type.name().toString(); if (type instanceof VoidType || type instanceof PrimitiveType || type instanceof UnresolvedTypeVariable || @@ -146,20 +147,20 @@ private void addReflectiveHierarchy(CombinedIndexBuildItem combinedIndexBuildIte return; } - addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, source, type.name(), + addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, newSource, type.name(), type.name(), processedReflectiveHierarchies, unindexedClasses, finalFieldsWritable, reflectiveClass, visits); for (ClassInfo subclass : combinedIndexBuildItem.getIndex().getAllKnownSubclasses(type.name())) { - addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, source, + addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, newSource, subclass.name(), subclass.name(), processedReflectiveHierarchies, unindexedClasses, finalFieldsWritable, reflectiveClass, visits); } for (ClassInfo subclass : combinedIndexBuildItem.getIndex().getAllKnownImplementors(type.name())) { - addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, source, + addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, newSource, subclass.name(), subclass.name(), processedReflectiveHierarchies, @@ -167,13 +168,14 @@ private void addReflectiveHierarchy(CombinedIndexBuildItem combinedIndexBuildIte } } else if (type instanceof ArrayType) { visits.addLast(() -> addReflectiveHierarchy(combinedIndexBuildItem, capabilities, - reflectiveHierarchyBuildItem, source, + reflectiveHierarchyBuildItem, newSource, type.asArrayType().constituent(), processedReflectiveHierarchies, unindexedClasses, finalFieldsWritable, reflectiveClass, visits)); } else if (type instanceof ParameterizedType) { if (!reflectiveHierarchyBuildItem.getIgnoreTypePredicate().test(type.name())) { - addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, source, type.name(), + addClassTypeHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, newSource, + type.name(), type.name(), processedReflectiveHierarchies, unindexedClasses, finalFieldsWritable, reflectiveClass, visits); @@ -181,7 +183,8 @@ private void addReflectiveHierarchy(CombinedIndexBuildItem combinedIndexBuildIte final ParameterizedType parameterizedType = (ParameterizedType) type; for (Type typeArgument : parameterizedType.arguments()) { visits.addLast( - () -> addReflectiveHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, source, + () -> addReflectiveHierarchy(combinedIndexBuildItem, capabilities, reflectiveHierarchyBuildItem, + newSource, typeArgument, processedReflectiveHierarchies, unindexedClasses, finalFieldsWritable, reflectiveClass, visits)); @@ -223,7 +226,9 @@ private void addClassTypeHierarchy(CombinedIndexBuildItem combinedIndexBuildItem .builder(name.toString()) .methods() .fields() + .classes() .serialization(reflectiveHierarchyBuildItem.isSerialization()) + .reason(source) .build()); processedReflectiveHierarchies.add(name); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForProxyBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForProxyBuildStep.java new file mode 100644 index 00000000000000..9b3f27f9923f58 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForProxyBuildStep.java @@ -0,0 +1,34 @@ +package io.quarkus.deployment.steps; + +import java.util.ArrayList; + +import org.jboss.jandex.DotName; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; +import io.quarkus.runtime.annotations.RegisterForProxy; + +public class RegisterForProxyBuildStep { + + @BuildStep + public void build(CombinedIndexBuildItem combinedIndexBuildItem, + BuildProducer proxy) { + for (var annotationInstance : combinedIndexBuildItem.getIndex() + .getAnnotations(DotName.createSimple(RegisterForProxy.class.getName()))) { + var targetsValue = annotationInstance.value("targets"); + var types = new ArrayList(); + if (targetsValue == null) { + var classInfo = annotationInstance.target().asClass(); + types.add(classInfo.name().toString()); + classInfo.interfaceNames().forEach(dotName -> types.add(dotName.toString())); + } else { + for (var type : targetsValue.asClassArray()) { + types.add(type.name().toString()); + } + } + proxy.produce(new NativeImageProxyDefinitionBuildItem(types)); + } + } +} \ No newline at end of file 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 c1a56f9fba3e13..f6fe3c8237d9b7 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 @@ -28,7 +28,6 @@ import io.quarkus.deployment.builditem.nativeimage.LambdaCapturingTypeBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem.Builder; import io.quarkus.deployment.util.JandexUtil; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -42,7 +41,6 @@ public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities ca BuildProducer reflectiveClassHierarchy, BuildProducer lambdaCapturingTypeProducer) { - ReflectiveHierarchyBuildItem.Builder builder = new ReflectiveHierarchyBuildItem.Builder(); Set processedReflectiveHierarchies = new HashSet(); IndexView index = combinedIndexBuildItem.getComputingIndex(); @@ -77,8 +75,7 @@ public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities ca } registerClass(classLoader, classInfo.name().toString(), methods, fields, ignoreNested, serialization, unsafeAllocated, reflectiveClass, reflectiveClassHierarchy, processedReflectiveHierarchies, - registerFullHierarchyValue, - builder); + registerFullHierarchyValue); continue; } @@ -87,7 +84,7 @@ public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities ca for (Type type : targets) { registerClass(classLoader, type.name().toString(), methods, fields, ignoreNested, serialization, unsafeAllocated, reflectiveClass, reflectiveClassHierarchy, processedReflectiveHierarchies, - registerFullHierarchyValue, builder); + registerFullHierarchyValue); } } @@ -96,7 +93,7 @@ public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities ca for (String className : classNames) { registerClass(classLoader, className, methods, fields, ignoreNested, serialization, unsafeAllocated, reflectiveClass, - reflectiveClassHierarchy, processedReflectiveHierarchies, registerFullHierarchyValue, builder); + reflectiveClassHierarchy, processedReflectiveHierarchies, registerFullHierarchyValue); } } } @@ -115,7 +112,7 @@ private void registerClass(ClassLoader classLoader, String className, boolean me boolean ignoreNested, boolean serialization, boolean unsafeAllocated, final BuildProducer reflectiveClass, BuildProducer reflectiveClassHierarchy, Set processedReflectiveHierarchies, - boolean registerFullHierarchyValue, Builder builder) { + boolean registerFullHierarchyValue) { reflectiveClass.produce(serialization ? ReflectiveClassBuildItem.builder(className).serialization().unsafeAllocated(unsafeAllocated).build() : ReflectiveClassBuildItem.builder(className).constructors().methods(methods).fields(fields) @@ -123,7 +120,7 @@ private void registerClass(ClassLoader classLoader, String className, boolean me //Search all class hierarchy, fields and methods in order to register its classes for reflection if (registerFullHierarchyValue) { - registerClassDependencies(reflectiveClassHierarchy, classLoader, processedReflectiveHierarchies, methods, builder, + registerClassDependencies(reflectiveClassHierarchy, classLoader, processedReflectiveHierarchies, methods, className); } @@ -136,7 +133,7 @@ private void registerClass(ClassLoader classLoader, String className, boolean me for (Class clazz : declaredClasses) { registerClass(classLoader, clazz.getName(), methods, fields, false, serialization, unsafeAllocated, reflectiveClass, - reflectiveClassHierarchy, processedReflectiveHierarchies, registerFullHierarchyValue, builder); + reflectiveClassHierarchy, processedReflectiveHierarchies, registerFullHierarchyValue); } } catch (ClassNotFoundException e) { log.warnf(e, "Failed to load Class %s", className); @@ -145,7 +142,6 @@ private void registerClass(ClassLoader classLoader, String className, boolean me private void registerClassDependencies(BuildProducer reflectiveClassHierarchy, ClassLoader classLoader, Set processedReflectiveHierarchies, boolean methods, - ReflectiveHierarchyBuildItem.Builder builder, String className) { try { DotName dotName = DotName.createSimple(className); @@ -154,10 +150,7 @@ private void registerClassDependencies(BuildProducer reflectiveClassHierarchy, ClassLoader classLoader, Set processedReflectiveHierarchies, IndexView indexView, DotName initialName, - ReflectiveHierarchyBuildItem.Builder builder, ClassInfo classInfo, boolean methods) { + ClassInfo classInfo, boolean methods) { List methodList = classInfo.methods(); for (MethodInfo methodInfo : methodList) { // we will only consider potential getters @@ -187,14 +180,14 @@ private void addMethodsForReflection(BuildProducer continue; } registerType(reflectiveClassHierarchy, classLoader, processedReflectiveHierarchies, - methods, builder, + methods, getMethodReturnType(indexView, initialName, classInfo, methodInfo)); } } private void registerClassFields(BuildProducer reflectiveClassHierarchy, ClassLoader classLoader, Set processedReflectiveHierarchies, IndexView indexView, DotName initialName, - ReflectiveHierarchyBuildItem.Builder builder, ClassInfo classInfo, boolean methods) { + ClassInfo classInfo, boolean methods) { List fieldList = classInfo.fields(); for (FieldInfo fieldInfo : fieldList) { if (Modifier.isStatic(fieldInfo.flags()) || @@ -202,20 +195,19 @@ private void registerClassFields(BuildProducer ref continue; } registerType(reflectiveClassHierarchy, classLoader, processedReflectiveHierarchies, - methods, builder, - fieldInfo.type()); + methods, fieldInfo.type()); } } private void registerType(BuildProducer reflectiveClassHierarchy, ClassLoader classLoader, Set processedReflectiveHierarchies, boolean methods, - ReflectiveHierarchyBuildItem.Builder builder, Type type) { + Type type) { if (type.kind().equals(Kind.ARRAY)) { type = type.asArrayType().constituent(); } if (type.kind() != Kind.PRIMITIVE && !processedReflectiveHierarchies.contains(type.name())) { registerClassDependencies(reflectiveClassHierarchy, classLoader, processedReflectiveHierarchies, - methods, builder, type.name().toString()); + methods, type.name().toString()); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/RuntimeConfigSetupBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/RuntimeConfigSetupBuildStep.java index ee549589ec1a16..ef5fcb0467473e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/RuntimeConfigSetupBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/RuntimeConfigSetupBuildStep.java @@ -10,14 +10,11 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; -import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.TryBlock; import io.quarkus.runtime.StartupContext; import io.quarkus.runtime.StartupTask; -import io.quarkus.runtime.configuration.ConfigurationException; public class RuntimeConfigSetupBuildStep { private static final String RUNTIME_CONFIG_STARTUP_TASK_CLASS_NAME = "io.quarkus.deployment.steps.RuntimeConfigSetup"; @@ -40,17 +37,11 @@ void setupRuntimeConfig( .interfaces(StartupTask.class).build()) { try (MethodCreator method = clazz.getMethodCreator("deploy", void.class, StartupContext.class)) { - TryBlock tryBlock = method.tryBlock(); - tryBlock.invokeVirtualMethod( - ofMethod(StartupContext.class, "setCurrentBuildStepName", void.class, String.class), + method.invokeVirtualMethod(ofMethod(StartupContext.class, "setCurrentBuildStepName", void.class, String.class), method.getMethodParam(0), method.load("RuntimeConfigSetupBuildStep.setupRuntimeConfig")); - tryBlock.invokeStaticMethod(C_CREATE_RUN_TIME_CONFIG); - tryBlock.returnValue(null); - - CatchBlockCreator cb = tryBlock.addCatch(RuntimeException.class); - cb.throwException(ConfigurationException.class, "Failed to read configuration properties", - cb.getCaughtException()); + method.invokeStaticMethod(C_CREATE_RUN_TIME_CONFIG); + method.returnValue(null); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/types/TypeParser.java b/core/deployment/src/main/java/io/quarkus/deployment/types/TypeParser.java new file mode 100644 index 00000000000000..3e79d19e6f38d8 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/types/TypeParser.java @@ -0,0 +1,233 @@ +package io.quarkus.deployment.types; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.quarkus.runtime.types.GenericArrayTypeImpl; +import io.quarkus.runtime.types.ParameterizedTypeImpl; +import io.quarkus.runtime.types.WildcardTypeImpl; + +/** + * Creates a {@link Type} by parsing the given string according to the following grammar: + * + *

+ * Type -> VoidType | PrimitiveType | ReferenceType
+ * VoidType -> 'void'
+ * PrimitiveType -> 'boolean' | 'byte' | 'short' | 'int'
+ *                | 'long' | 'float' | 'double' | 'char'
+ * ReferenceType -> PrimitiveType ('[' ']')+
+ *                | ClassType ('<' TypeArgument (',' TypeArgument)* '>')? ('[' ']')*
+ * ClassType -> FULLY_QUALIFIED_NAME
+ * TypeArgument -> ReferenceType | WildcardType
+ * WildcardType -> '?' | '?' ('extends' | 'super') ReferenceType
+ * 
+ * + * Notice that the resulting type never contains type variables, only "proper" types. + * Also notice that the grammar above does not support all kinds of nested types; + * it should be possible to add that later, if there's an actual need. + *

+ * Types produced by this parser can be transferred from build time to runtime + * via the recorder mechanism. + */ +public class TypeParser { + public static Type parse(String str) { + return new TypeParser(str).parse(); + } + + private final String str; + + private int pos = 0; + + private TypeParser(String str) { + this.str = Objects.requireNonNull(str); + } + + private Type parse() { + Type result; + + String token = nextToken(); + if (token.isEmpty()) { + throw unexpected(token); + } else if (token.equals("void")) { + result = void.class; + } else if (isPrimitiveType(token) && peekToken().isEmpty()) { + result = parsePrimitiveType(token); + } else { + result = parseReferenceType(token); + } + + expect(""); + return result; + } + + private Type parseReferenceType(String token) { + if (isPrimitiveType(token)) { + Type primitive = parsePrimitiveType(token); + return parseArrayType(primitive); + } else if (isClassType(token)) { + Type result = parseClassType(token); + if (peekToken().equals("<")) { + expect("<"); + List typeArguments = new ArrayList<>(); + typeArguments.add(parseTypeArgument()); + while (peekToken().equals(",")) { + expect(","); + typeArguments.add(parseTypeArgument()); + } + expect(">"); + result = new ParameterizedTypeImpl(result, typeArguments.toArray(Type[]::new)); + } + if (peekToken().equals("[")) { + return parseArrayType(result); + } + return result; + } else { + throw unexpected(token); + } + } + + private Type parseArrayType(Type elementType) { + expect("["); + expect("]"); + int dimensions = 1; + while (peekToken().equals("[")) { + expect("["); + expect("]"); + dimensions++; + } + + if (elementType instanceof Class clazz) { + return parseClassType("[".repeat(dimensions) + + (clazz.isPrimitive() ? clazz.descriptorString() : "L" + clazz.getName() + ";")); + } else { + Type result = elementType; + for (int i = 0; i < dimensions; i++) { + result = new GenericArrayTypeImpl(result); + } + return result; + } + } + + private Type parseTypeArgument() { + String token = nextToken(); + if (token.equals("?")) { + if (peekToken().equals("extends")) { + expect("extends"); + Type bound = parseReferenceType(nextToken()); + return WildcardTypeImpl.withUpperBound(bound); + } else if (peekToken().equals("super")) { + expect("super"); + Type bound = parseReferenceType(nextToken()); + return WildcardTypeImpl.withLowerBound(bound); + } else { + return WildcardTypeImpl.defaultInstance(); + } + } else { + return parseReferenceType(token); + } + } + + private boolean isPrimitiveType(String token) { + return token.equals("boolean") + || token.equals("byte") + || token.equals("short") + || token.equals("int") + || token.equals("long") + || token.equals("float") + || token.equals("double") + || token.equals("char"); + } + + private Type parsePrimitiveType(String token) { + return switch (token) { + case "boolean" -> boolean.class; + case "byte" -> byte.class; + case "short" -> short.class; + case "int" -> int.class; + case "long" -> long.class; + case "float" -> float.class; + case "double" -> double.class; + case "char" -> char.class; + default -> throw unexpected(token); + }; + } + + private boolean isClassType(String token) { + return !token.isEmpty() && Character.isJavaIdentifierStart(token.charAt(0)); + } + + private Type parseClassType(String token) { + try { + return Class.forName(token, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unknown class: " + token, e); + } + } + + // --- + + private void expect(String expected) { + String token = nextToken(); + if (!expected.equals(token)) { + throw unexpected(token); + } + } + + private IllegalArgumentException unexpected(String token) { + if (token.isEmpty()) { + throw new IllegalArgumentException("Unexpected end of input: " + str); + } + return new IllegalArgumentException("Unexpected token '" + token + "' at position " + (pos - token.length()) + + ": " + str); + } + + private String peekToken() { + // skip whitespace + while (pos < str.length() && Character.isWhitespace(str.charAt(pos))) { + pos++; + } + + // end of input + if (pos == str.length()) { + return ""; + } + + int pos = this.pos; + + // current char is a token on its own + if (isSpecial(str.charAt(pos))) { + return str.substring(pos, pos + 1); + } + + // token is a keyword or fully qualified name + int begin = pos; + while (pos < str.length() && Character.isJavaIdentifierStart(str.charAt(pos))) { + do { + pos++; + } while (pos < str.length() && Character.isJavaIdentifierPart(str.charAt(pos))); + + if (pos < str.length() && str.charAt(pos) == '.') { + pos++; + } else { + return str.substring(begin, pos); + } + } + + if (pos == str.length()) { + throw new IllegalArgumentException("Unexpected end of input: " + str); + } + throw new IllegalArgumentException("Unexpected character '" + str.charAt(pos) + "' at position " + pos + ": " + str); + } + + private String nextToken() { + String result = peekToken(); + pos += result.length(); + return result; + } + + private boolean isSpecial(char c) { + return c == ',' || c == '?' || c == '<' || c == '>' || c == '[' || c == ']'; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ArtifactResourceResolver.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ArtifactResourceResolver.java new file mode 100644 index 00000000000000..2eb6b98a21ed6b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ArtifactResourceResolver.java @@ -0,0 +1,101 @@ +package io.quarkus.deployment.util; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactCoordsPattern; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathFilter; +import io.quarkus.paths.PathVisit; +import io.quarkus.util.GlobUtil; + +/** + * Utility class to extract a list of resource paths from a given artifact and path. + */ +public final class ArtifactResourceResolver { + private final Collection artifacts; + + /** + * Creates a {@code ArtifactResourceResolver} for the given artifact + * + * @param dependencies the resolved dependencies of the build + * @param artifactCoordinates the coordinates of the artifact containing the resources + */ + public static ArtifactResourceResolver of( + Collection dependencies, ArtifactCoords artifactCoordinates) { + + return new ArtifactResourceResolver(dependencies, List.of(artifactCoordinates)); + } + + /** + * Creates a {@code ArtifactResourceResolver} for the given artifact + * + * @param dependencies the resolved dependencies of the build + * @param artifactCoordinatesCollection a coordinates {@link Collection} for the artifacts containing the resources + */ + public static ArtifactResourceResolver of( + Collection dependencies, Collection artifactCoordinatesCollection) { + + return new ArtifactResourceResolver(dependencies, artifactCoordinatesCollection); + } + + private ArtifactResourceResolver( + Collection dependencies, Collection artifactCoordinates) { + + var patterns = ArtifactCoordsPattern.toPatterns(artifactCoordinates); + artifacts = patterns.stream() + .map(p -> findArtifact(dependencies, p)) + .collect(Collectors.toSet()); + } + + private static ResolvedDependency findArtifact( + Collection dependencies, ArtifactCoordsPattern pattern) { + + return dependencies.stream() + .filter(pattern::matches) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "%s artifact not found".formatted(pattern.toString()))); + } + + /** + * Extracts a {@link Collection} of resource paths with the given filter + * + * @param pathFilter the filter for the resources in glob syntax (see {@link GlobUtil}) + * @return a collection of the found resource paths + */ + public Collection resourcePathList(PathFilter pathFilter) { + return artifacts.stream() + .map(a -> pathsForArtifact(a, pathFilter)) + .flatMap(Collection::stream) + .toList(); + } + + private Collection pathsForArtifact(ResolvedDependency artifact, PathFilter pathFilter) { + var pathList = new ArrayList(); + var pathTree = artifact.getContentTree(pathFilter); + pathTree.walk(visit -> pathList.add(relativePath(visit))); + return pathList; + } + + private Path relativePath(PathVisit visit) { + var path = visit.getPath(); + return path.getRoot().relativize(path); + } + + /** + * Extracts a {@link List} of resource paths as strings with the given filter + * + * @param pathFilter the filter for the resources in glob syntax (see {@link GlobUtil}) + * @return a list of the found resource paths as strings + */ + public List resourceList(PathFilter pathFilter) { + return resourcePathList(pathFilter).stream() + .map(Path::toString) + .toList(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java index 10d33810a02a05..5cf44bc7adb337 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java @@ -6,6 +6,7 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -46,17 +47,20 @@ public static ContainerRuntime detectContainerRuntime() { return detectContainerRuntime(true); } - public static ContainerRuntime detectContainerRuntime(List orderToCheckRuntimes) { + public static ContainerRuntime detectContainerRuntime(ContainerRuntime... orderToCheckRuntimes) { return detectContainerRuntime(true, orderToCheckRuntimes); } - public static ContainerRuntime detectContainerRuntime(boolean required) { - return detectContainerRuntime(required, List.of(ContainerRuntime.DOCKER, ContainerRuntime.PODMAN)); + public static ContainerRuntime detectContainerRuntime(boolean required, ContainerRuntime... orderToCheckRuntimes) { + return detectContainerRuntime( + required, + ((orderToCheckRuntimes != null) && (orderToCheckRuntimes.length > 0)) ? Arrays.asList(orderToCheckRuntimes) + : List.of(ContainerRuntime.DOCKER, ContainerRuntime.PODMAN)); } public static ContainerRuntime detectContainerRuntime(boolean required, List orderToCheckRuntimes) { ContainerRuntime containerRuntime = loadContainerRuntimeFromSystemProperty(); - if (containerRuntime != null) { + if ((containerRuntime != null) && orderToCheckRuntimes.contains(containerRuntime)) { return containerRuntime; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/IoUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/IoUtil.java index 132efb7da3d528..a4400f8a8b79c9 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/IoUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/IoUtil.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.util; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.IOException; import java.io.InputStream; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ServiceUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ServiceUtil.java index b45ee1f1f52f96..16f396f3efe0c1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/ServiceUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ServiceUtil.java @@ -19,6 +19,7 @@ /** */ public final class ServiceUtil { + private ServiceUtil() { } @@ -59,6 +60,14 @@ public static Set classNamesNamedIn(Path path) throws IOException { return set; } + public static Set classNamesNamedIn(String filePath) { + try { + return classNamesNamedIn(Thread.currentThread().getContextClassLoader(), filePath); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + /** * - Lines starting by a # (or white spaces and a #) are ignored. - For * lines containing data before a comment (#) are parsed and only the value diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index ebe88aa8208730..9e252dcb0fb333 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -1,15 +1,16 @@ package io.quarkus.runner.bootstrap; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; @@ -30,8 +31,10 @@ import io.quarkus.bootstrap.app.ClassChangeInformation; import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.app.SbomResult; import io.quarkus.bootstrap.classloading.ClassLoaderEventListener; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.bootstrap.util.PropertyUtils; import io.quarkus.builder.BuildChainBuilder; import io.quarkus.builder.BuildResult; import io.quarkus.builder.item.BuildItem; @@ -48,6 +51,7 @@ import io.quarkus.deployment.pkg.builditem.DeploymentResultBuildItem; import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; +import io.quarkus.deployment.sbom.SbomBuildItem; import io.quarkus.dev.spi.DevModeType; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.QuarkusConfigFactory; @@ -138,29 +142,30 @@ public AugmentActionImpl(CuratedApplication curatedApplication, List[] targets = Arrays.stream(finalOutputs) - .map(new Function>() { - @Override - public Class apply(String s) { - try { - return (Class) Class.forName(s, false, classLoader); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); + try (QuarkusClassLoader classLoader = curatedApplication.createDeploymentClassLoader()) { + Class[] targets = Arrays.stream(finalOutputs) + .map(new Function>() { + @Override + public Class apply(String s) { + try { + return (Class) Class.forName(s, false, classLoader); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } } - } - }).toArray(Class[]::new); - BuildResult result = runAugment(true, Collections.emptySet(), null, classLoader, targets); + }).toArray(Class[]::new); + BuildResult result = runAugment(true, Collections.emptySet(), null, classLoader, targets); - writeDebugSourceFile(result); - try { - BiConsumer consumer = (BiConsumer) Class - .forName(resultHandler, false, classLoader) - .getConstructor().newInstance(); - consumer.accept(context, result); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException - | InvocationTargetException e) { - throw new RuntimeException(e); + writeDebugSourceFile(result); + try { + BiConsumer consumer = (BiConsumer) Class + .forName(resultHandler, false, classLoader) + .getConstructor().newInstance(); + consumer.accept(context, result); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException + | InvocationTargetException e) { + throw new RuntimeException(e); + } } } @@ -169,35 +174,50 @@ public AugmentResult createProductionApplication() { if (launchMode != LaunchMode.NORMAL) { throw new IllegalStateException("Can only create a production application when using NORMAL launch mode"); } - ClassLoader classLoader = curatedApplication.createDeploymentClassLoader(); - BuildResult result = runAugment(true, Collections.emptySet(), null, classLoader, ArtifactResultBuildItem.class, - DeploymentResultBuildItem.class); + try (QuarkusClassLoader classLoader = curatedApplication.createDeploymentClassLoader()) { + BuildResult result = runAugment(true, Collections.emptySet(), null, classLoader, ArtifactResultBuildItem.class, + DeploymentResultBuildItem.class, SbomBuildItem.class); + + writeDebugSourceFile(result); - writeDebugSourceFile(result); + JarBuildItem jarBuildItem = result.consumeOptional(JarBuildItem.class); + NativeImageBuildItem nativeImageBuildItem = result.consumeOptional(NativeImageBuildItem.class); + List artifactResultBuildItems = result.consumeMulti(ArtifactResultBuildItem.class); + BuildSystemTargetBuildItem buildSystemTargetBuildItem = result.consume(BuildSystemTargetBuildItem.class); + Map> sboms = getSboms(result.consumeMulti(SbomBuildItem.class)); - JarBuildItem jarBuildItem = result.consumeOptional(JarBuildItem.class); - NativeImageBuildItem nativeImageBuildItem = result.consumeOptional(NativeImageBuildItem.class); - List artifactResultBuildItems = result.consumeMulti(ArtifactResultBuildItem.class); - BuildSystemTargetBuildItem buildSystemTargetBuildItem = result.consume(BuildSystemTargetBuildItem.class); + // this depends on the fact that the order in which we can obtain MultiBuildItems is the same as they are produced + // we want to write result of the final artifact created + if (artifactResultBuildItems.isEmpty()) { + throw new IllegalStateException("No artifact results were produced"); + } + ArtifactResultBuildItem lastResult = artifactResultBuildItems.get(artifactResultBuildItems.size() - 1); + writeArtifactResultMetadataFile(buildSystemTargetBuildItem, lastResult); - // this depends on the fact that the order in which we can obtain MultiBuildItems is the same as they are produced - // we want to write result of the final artifact created - if (artifactResultBuildItems.isEmpty()) { - throw new IllegalStateException("No artifact results were produced"); + return new AugmentResult(artifactResultBuildItems.stream() + .map(a -> new ArtifactResult(a.getPath(), a.getType(), a.getMetadata())) + .collect(Collectors.toList()), + jarBuildItem != null ? jarBuildItem.toJarResult(sboms.getOrDefault(jarBuildItem.getPath(), List.of())) + : null, + nativeImageBuildItem != null ? nativeImageBuildItem.getPath() : null, + nativeImageBuildItem != null ? nativeImageBuildItem.getGraalVMInfo().toMap() : Map.of()); } - ArtifactResultBuildItem lastResult = artifactResultBuildItems.get(artifactResultBuildItems.size() - 1); - writeArtifactResultMetadataFile(buildSystemTargetBuildItem, lastResult); + } - return new AugmentResult(artifactResultBuildItems.stream() - .map(a -> new ArtifactResult(a.getPath(), a.getType(), a.getMetadata())) - .collect(Collectors.toList()), - jarBuildItem != null ? jarBuildItem.toJarResult() : null, - nativeImageBuildItem != null ? nativeImageBuildItem.getPath() : null, - nativeImageBuildItem != null ? nativeImageBuildItem.getGraalVMInfo().toMap() : Collections.emptyMap()); + private Map> getSboms(List sbomBuildItems) { + if (sbomBuildItems.isEmpty()) { + return Map.of(); + } + final Map> result = new HashMap<>(); + for (var sbomBuildItem : sbomBuildItems) { + result.computeIfAbsent(sbomBuildItem.getResult().getApplicationRunner(), p -> new ArrayList<>()) + .add(sbomBuildItem.getResult()); + } + return result; } private void writeDebugSourceFile(BuildResult result) { - String debugSourcesDir = BootstrapDebug.DEBUG_SOURCES_DIR; + String debugSourcesDir = BootstrapDebug.debugSourcesDir(); if (debugSourcesDir != null) { for (GeneratedClassBuildItem i : result.consumeMulti(GeneratedClassBuildItem.class)) { try { @@ -235,8 +255,8 @@ private void writeArtifactResultMetadataFile(BuildSystemTargetBuildItem outputTa properties.put("metadata." + entry.getKey(), entry.getValue()); } } - try (FileOutputStream fos = new FileOutputStream(quarkusArtifactMetadataPath.toFile())) { - properties.store(fos, "Generated by Quarkus - Do not edit manually"); + try { + PropertyUtils.store(properties, quarkusArtifactMetadataPath, "Generated by Quarkus - Do not edit manually"); } catch (IOException e) { log.debug("Unable to write artifact result metadata file", e); } @@ -247,10 +267,11 @@ public StartupActionImpl createInitialRuntimeApplication() { if (launchMode == LaunchMode.NORMAL) { throw new IllegalStateException("Cannot launch a runtime application with NORMAL launch mode"); } - ClassLoader classLoader = curatedApplication.createDeploymentClassLoader(); - @SuppressWarnings("unchecked") - BuildResult result = runAugment(true, Collections.emptySet(), null, classLoader, NON_NORMAL_MODE_OUTPUTS); - return new StartupActionImpl(curatedApplication, result); + try (QuarkusClassLoader classLoader = curatedApplication.createDeploymentClassLoader()) { + @SuppressWarnings("unchecked") + BuildResult result = runAugment(true, Collections.emptySet(), null, classLoader, NON_NORMAL_MODE_OUTPUTS); + return new StartupActionImpl(curatedApplication, result); + } } @Override @@ -259,13 +280,14 @@ public StartupActionImpl reloadExistingApplication(boolean hasStartedSuccessfull if (launchMode != LaunchMode.DEVELOPMENT) { throw new IllegalStateException("Only application with launch mode DEVELOPMENT can restart"); } - ClassLoader classLoader = curatedApplication.createDeploymentClassLoader(); - @SuppressWarnings("unchecked") - BuildResult result = runAugment(!hasStartedSuccessfully, changedResources, classChangeInformation, classLoader, - NON_NORMAL_MODE_OUTPUTS); + try (QuarkusClassLoader classLoader = curatedApplication.createDeploymentClassLoader()) { + @SuppressWarnings("unchecked") + BuildResult result = runAugment(!hasStartedSuccessfully, changedResources, classChangeInformation, classLoader, + NON_NORMAL_MODE_OUTPUTS); - return new StartupActionImpl(curatedApplication, result); + return new StartupActionImpl(curatedApplication, result); + } } private BuildResult runAugment(boolean firstRun, Set changedResources, @@ -273,7 +295,7 @@ private BuildResult runAugment(boolean firstRun, Set changedResources, Class... finalOutputs) { ClassLoader old = Thread.currentThread().getContextClassLoader(); try { - QuarkusClassLoader classLoader = curatedApplication.getAugmentClassLoader(); + QuarkusClassLoader classLoader = curatedApplication.getOrCreateAugmentClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); LaunchMode.set(launchMode); @@ -283,7 +305,8 @@ private BuildResult runAugment(boolean firstRun, Set changedResources, .setTargetDir(quarkusBootstrap.getTargetDirectory()) .setDeploymentClassLoader(deploymentClassLoader) .setBuildSystemProperties(quarkusBootstrap.getBuildSystemProperties()) - .setEffectiveModel(curatedApplication.getApplicationModel()); + .setEffectiveModel(curatedApplication.getApplicationModel()) + .setDependencyInfoProvider(quarkusBootstrap.getDependencyInfoProvider()); if (quarkusBootstrap.getBaseName() != null) { builder.setBaseName(quarkusBootstrap.getBaseName()); } diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java index 96ef7efc1c3ec9..8287573c246e8c 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java @@ -1,6 +1,6 @@ package io.quarkus.runner.bootstrap; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import java.io.Closeable; import java.io.File; @@ -11,11 +11,13 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -52,6 +54,7 @@ public class StartupActionImpl implements StartupAction { private final String applicationClassName; private final Map devServicesProperties; private final List runtimeApplicationShutdownBuildItems; + private final List runtimeCloseTasks = new ArrayList<>(); public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buildResult) { this.curatedApplication = curatedApplication; @@ -62,7 +65,7 @@ public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buil this.runtimeApplicationShutdownBuildItems = buildResult.consumeMulti(RuntimeApplicationShutdownBuildItem.class); Map transformedClasses = extractTransformedClasses(buildResult); - QuarkusClassLoader baseClassLoader = curatedApplication.getBaseRuntimeClassLoader(); + QuarkusClassLoader baseClassLoader = curatedApplication.getOrCreateBaseRuntimeClassLoader(); QuarkusClassLoader runtimeClassLoader; //so we have some differences between dev and test mode here. @@ -125,6 +128,13 @@ public void run() { log.error("Failed to run close task", t); } } + for (var closeTask : runtimeCloseTasks) { + try { + closeTask.close(); + } catch (Throwable t) { + log.error("Failed to run close task", t); + } + } } } }, "Quarkus Main Thread"); @@ -168,6 +178,11 @@ public void run() { } } + @Override + public void addRuntimeCloseTask(Closeable closeTask) { + this.runtimeCloseTasks.add(closeTask); + } + private void doClose() { try { runtimeClassLoader.loadClass(Quarkus.class.getName()).getMethod("blockingExit").invoke(null); @@ -196,24 +211,20 @@ public int runMainClassBlocking(String... args) throws Exception { try { AtomicInteger result = new AtomicInteger(); Class lifecycleManager = Class.forName(ApplicationLifecycleManager.class.getName(), true, runtimeClassLoader); - Method getCurrentApplication = lifecycleManager.getDeclaredMethod("getCurrentApplication"); - Object oldApplication = getCurrentApplication.invoke(null); - lifecycleManager.getDeclaredMethod("setDefaultExitCodeHandler", Consumer.class).invoke(null, - new Consumer() { - @Override - public void accept(Integer integer) { - result.set(integer); - } - }); - // force init here - Class appClass = Class.forName(className, true, runtimeClassLoader); - Method start = appClass.getMethod("main", String[].class); - start.invoke(null, (Object) (args == null ? new String[0] : args)); + AtomicBoolean alreadyStarted = new AtomicBoolean(); + Method setDefaultExitCodeHandler = lifecycleManager.getDeclaredMethod("setDefaultExitCodeHandler", Consumer.class); + Method setAlreadyStartedCallback = lifecycleManager.getDeclaredMethod("setAlreadyStartedCallback", Consumer.class); - CountDownLatch latch = new CountDownLatch(1); - new Thread(new Runnable() { - @Override - public void run() { + try { + setDefaultExitCodeHandler.invoke(null, (Consumer) result::set); + setAlreadyStartedCallback.invoke(null, (Consumer) alreadyStarted::set); + // force init here + Class appClass = Class.forName(className, true, runtimeClassLoader); + Method start = appClass.getMethod("main", String[].class); + start.invoke(null, (Object) (args == null ? new String[0] : args)); + + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { try { Class q = Class.forName(Quarkus.class.getName(), true, runtimeClassLoader); q.getMethod("blockingExit").invoke(null); @@ -222,18 +233,27 @@ public void run() { } finally { latch.countDown(); } + }).start(); + latch.await(); + + if (alreadyStarted.get()) { + //quarkus was not actually started by the main method + //just return + return 0; } - }).start(); - latch.await(); - - Object newApplication = getCurrentApplication.invoke(null); - if (oldApplication == newApplication) { - //quarkus was not actually started by the main method - //just return - return 0; + return result.get(); + } finally { + setDefaultExitCodeHandler.invoke(null, (Consumer) null); + setAlreadyStartedCallback.invoke(null, (Consumer) null); } - return result.get(); } finally { + for (var closeTask : runtimeCloseTasks) { + try { + closeTask.close(); + } catch (Throwable t) { + log.error("Failed to run close task", t); + } + } runtimeClassLoader.close(); Thread.currentThread().setContextClassLoader(old); for (var i : runtimeApplicationShutdownBuildItems) { @@ -294,6 +314,13 @@ public void close() throws IOException { // (e.g. ServiceLoader calls) Thread.currentThread().setContextClassLoader(runtimeClassLoader); closeTask.close(); + for (var closeTask : runtimeCloseTasks) { + try { + closeTask.close(); + } catch (Throwable t) { + log.error("Failed to run close task", t); + } + } } finally { Thread.currentThread().setContextClassLoader(original); runtimeClassLoader.close(); @@ -365,9 +392,10 @@ private static Map extractGeneratedResources(BuildResult buildRe for (GeneratedClassBuildItem i : buildResult.consumeMulti(GeneratedClassBuildItem.class)) { if (i.isApplicationClass() == applicationClasses) { data.put(fromClassNameToResourceName(i.getName()), i.getClassData()); - if (BootstrapDebug.DEBUG_CLASSES_DIR != null) { + var debugClassesDir = BootstrapDebug.debugClassesDir(); + if (debugClassesDir != null) { try { - File debugPath = new File(BootstrapDebug.DEBUG_CLASSES_DIR); + File debugPath = new File(debugClassesDir); if (!debugPath.exists()) { debugPath.mkdir(); } @@ -382,7 +410,7 @@ private static Map extractGeneratedResources(BuildResult buildRe } } - String debugSourcesDir = BootstrapDebug.DEBUG_SOURCES_DIR; + String debugSourcesDir = BootstrapDebug.debugSourcesDir(); if (debugSourcesDir != null) { try { if (i.getSource() != null) { diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/FileSystemWatcherTestCase.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/FileSystemWatcherTestCase.java index 8ea252fa07e54c..d605cb883cdd16 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/dev/FileSystemWatcherTestCase.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/FileSystemWatcherTestCase.java @@ -5,9 +5,12 @@ import static io.quarkus.deployment.dev.filesystem.watch.FileChangeEvent.Type.REMOVED; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.util.Collection; +import java.util.Comparator; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -33,8 +36,8 @@ public class FileSystemWatcherTestCase { private final BlockingDeque> results = new LinkedBlockingDeque<>(); private final BlockingDeque> secondResults = new LinkedBlockingDeque<>(); - File rootDir; - File existingSubDir; + Path rootDir; + Path existingSubDir; @BeforeEach public void setup() throws Exception { @@ -42,30 +45,24 @@ public void setup() throws Exception { //as it just relies on polling Assumptions.assumeTrue(RuntimeUpdatesProcessor.IS_LINUX); - rootDir = new File(System.getProperty("java.io.tmpdir") + DIR_NAME); + rootDir = Path.of(System.getProperty("java.io.tmpdir"), DIR_NAME); deleteRecursive(rootDir); - rootDir.mkdirs(); - File existing = new File(rootDir, EXISTING_FILE_NAME); + Files.createDirectories(rootDir); + Path existing = rootDir.resolve(EXISTING_FILE_NAME); touchFile(existing); - existingSubDir = new File(rootDir, EXISTING_DIR); - existingSubDir.mkdir(); - existing = new File(existingSubDir, EXISTING_FILE_NAME); + existingSubDir = rootDir.resolve(EXISTING_DIR); + Files.createDirectory(existingSubDir); + existing = existingSubDir.resolve(EXISTING_FILE_NAME); touchFile(existing); } - private static void touchFile(File existing) throws IOException { - FileOutputStream out = new FileOutputStream(existing); - try { - out.write(("data" + System.currentTimeMillis()).getBytes()); - out.flush(); - } finally { - out.close(); - } + private static void touchFile(Path existing) throws IOException { + Files.writeString(existing, "data" + System.currentTimeMillis()); } @AfterEach - public void after() { + public void after() throws IOException { if (rootDir != null) { deleteRecursive(rootDir); } @@ -75,48 +72,48 @@ public void after() { public void testFileSystemWatcher() throws Exception { WatchServiceFileSystemWatcher watcher = new WatchServiceFileSystemWatcher("test", true); try { - watcher.watchPath(rootDir, new FileChangeCallback() { + watcher.watchDirectoryRecursively(rootDir, new FileChangeCallback() { @Override public void handleChanges(Collection changes) { results.add(changes); } }); - watcher.watchPath(rootDir, new FileChangeCallback() { + watcher.watchDirectoryRecursively(rootDir, new FileChangeCallback() { @Override public void handleChanges(Collection changes) { secondResults.add(changes); } }); //first add a file - File added = new File(rootDir, "newlyAddedFile.txt").getAbsoluteFile(); + Path added = rootDir.resolve("newlyAddedFile.txt").toAbsolutePath(); touchFile(added); checkResult(added, ADDED); - added.setLastModified(500); + Files.setLastModifiedTime(added, FileTime.fromMillis(500)); checkResult(added, MODIFIED); - added.delete(); + Files.delete(added); Thread.sleep(1); checkResult(added, REMOVED); - added = new File(existingSubDir, "newSubDirFile.txt"); + added = existingSubDir.resolve("newSubDirFile.txt"); touchFile(added); checkResult(added, ADDED); - added.setLastModified(500); + Files.setLastModifiedTime(added, FileTime.fromMillis(500)); checkResult(added, MODIFIED); - added.delete(); + Files.delete(added); Thread.sleep(1); checkResult(added, REMOVED); - File existing = new File(rootDir, EXISTING_FILE_NAME); - existing.delete(); + Path existing = rootDir.resolve(EXISTING_FILE_NAME); + Files.delete(existing); Thread.sleep(1); checkResult(existing, REMOVED); - File newDir = new File(rootDir, "newlyCreatedDirectory"); - newDir.mkdir(); + Path newDir = rootDir.resolve("newlyCreatedDirectory"); + Files.createDirectory(newDir); checkResult(newDir, ADDED); - added = new File(newDir, "newlyAddedFileInNewlyAddedDirectory.txt").getAbsoluteFile(); + added = newDir.resolve("newlyAddedFileInNewlyAddedDirectory.txt").toAbsolutePath(); touchFile(added); checkResult(added, ADDED); - added.setLastModified(500); + Files.setLastModifiedTime(added, FileTime.fromMillis(500)); checkResult(added, MODIFIED); - added.delete(); + Files.delete(added); Thread.sleep(1); checkResult(added, REMOVED); @@ -126,7 +123,7 @@ public void handleChanges(Collection changes) { } - private void checkResult(File file, FileChangeEvent.Type type) throws InterruptedException { + private void checkResult(Path file, FileChangeEvent.Type type) throws InterruptedException { Collection results = this.results.poll(20, TimeUnit.SECONDS); Collection secondResults = this.secondResults.poll(20, TimeUnit.SECONDS); Assertions.assertNotNull(results); @@ -151,8 +148,8 @@ private void checkResult(File file, FileChangeEvent.Type type) throws Interrupte endTime = System.currentTimeMillis() + 10000; while (type == ADDED && (res.getType() == MODIFIED || res2.getType() == MODIFIED) - && (res.getFile().equals(file.getParentFile()) || res2.getFile().equals(file.getParentFile())) - && !file.isDirectory() + && (res.getFile().equals(file.getParent()) || res2.getFile().equals(file.getParent())) + && !Files.isDirectory(file) && System.currentTimeMillis() < endTime) { FileChangeEvent[] nextEvents = consumeEvents(); res = nextEvents[0]; @@ -179,14 +176,15 @@ private FileChangeEvent[] consumeEvents() throws InterruptedException { return nextEvents; } - public static void deleteRecursive(final File file) { - File[] files = file.listFiles(); - if (files != null) { - for (File f : files) { - deleteRecursive(f); - } + public static void deleteRecursive(final Path path) throws IOException { + if (!Files.exists(path)) { + return; } - file.delete(); + + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); } } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java index 0a0f28227bb267..3550674b52e3e8 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java @@ -225,11 +225,21 @@ public boolean enableDashboardDump() { return false; } + @Override + public boolean includeReasonsInConfigFiles() { + return false; + } + @Override public Compression compression() { return null; } + @Override + public boolean agentConfigurationApply() { + return false; + } + private class TestBuildImageConfig implements BuilderImageConfig { private final String image; private final ImagePullStrategy pull; diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/BannerProcessorTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/BannerProcessorTest.java index 37a61aa51272b6..2260e1bb3def4a 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/BannerProcessorTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/BannerProcessorTest.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.pkg.steps; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java index 9af2755056560e..647bd3a28474ba 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java @@ -165,6 +165,19 @@ public void testGraalVMEE22DevVersionParser() { assertThat(graalVMEE22Dev.javaVersion.update()).isEqualTo(0); } + @Test + public void testGraalVMEA24DevVersionParser() { + final Version graalVMEA24Dev = Version.of(Stream.of(("native-image 24-ea 2025-03-18\n" + + "OpenJDK Runtime Environment Oracle GraalVM 24-dev.ea+10.1 (build 24-ea+10-1076)\n" + + "OpenJDK 64-Bit Server VM Oracle GraalVM 24-dev.ea+10.1 (build 24-ea+10-1076, mixed mode, sharing)") + .split("\\n"))); + assertThat(graalVMEA24Dev.distribution.name()).isEqualTo("GRAALVM"); + assertThat(graalVMEA24Dev.getVersionAsString()).isEqualTo("24.2-dev"); + assertThat(graalVMEA24Dev.javaVersion.toString()).isEqualTo("24-ea+10-1076"); + assertThat(graalVMEA24Dev.javaVersion.feature()).isEqualTo(24); + assertThat(graalVMEA24Dev.javaVersion.update()).isEqualTo(0); + } + @Test public void testGraalVMVersionsOlderThan() { assertOlderThan("GraalVM Version 19.3.6 CE", "GraalVM Version 20.2.0 (Java Version 11.0.9)"); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java index 7cfb2c4ece4967..fc5180b34e9a72 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java @@ -2,15 +2,44 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.File; +import java.io.FileOutputStream; +import java.math.BigInteger; import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Calendar; +import java.util.Date; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; +import java.util.zip.ZipFile; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import jdk.security.jarsigner.JarSigner; + /** * Test for {@link JarResultBuildStep} */ @@ -18,11 +47,19 @@ class JarResultBuildStepTest { @Test void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception { - Path signedJarFilePath = Path.of(getClass().getClassLoader().getResource("signed.jar").toURI()); - Path jarFilePath = tempDir.resolve("unsigned.jar"); - JarResultBuildStep.filterJarFile(signedJarFilePath, jarFilePath, - Set.of("org/eclipse/jgit/transport/sshd/SshdSessionFactory.class")); - try (JarFile jarFile = new JarFile(jarFilePath.toFile())) { + JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "myarchive.jar") + .addClasses(Integer.class); + Path unsignedJarPath = tempDir.resolve("unsigned-jar.jar"); + Path signedJarPath = tempDir.resolve("signed-jar.jar"); + Path unsignedJarToTestPath = tempDir.resolve("unsigned.jar"); + archive.as(ZipExporter.class).exportTo(new File(unsignedJarPath.toUri()), true); + JarSigner signer = new JarSigner.Builder(createPrivateKeyEntry()).build(); + try (ZipFile in = new ZipFile(unsignedJarPath.toFile()); + FileOutputStream out = new FileOutputStream(signedJarPath.toFile())) { + signer.sign(in, out); + } + JarResultBuildStep.filterJarFile(signedJarPath, unsignedJarToTestPath, Set.of("java/lang/Integer.class")); + try (JarFile jarFile = new JarFile(unsignedJarToTestPath.toFile())) { assertThat(jarFile.stream().map(JarEntry::getName)).doesNotContain("META-INF/ECLIPSE_.RSA", "META-INF/ECLIPSE_.SF"); // Check that the manifest is still present Manifest manifest = jarFile.getManifest(); @@ -31,4 +68,59 @@ void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception { } } + @Test + void manifestTimeShouldAlwaysBeSetToEpoch(@TempDir Path tempDir) throws Exception { + JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "myarchive.jar") + .addClasses(Integer.class) + .addManifest(); + Path initialJar = tempDir.resolve("initial.jar"); + Path filteredJar = tempDir.resolve("filtered.jar"); + archive.as(ZipExporter.class).exportTo(new File(initialJar.toUri()), true); + JarResultBuildStep.filterJarFile(initialJar, filteredJar, Set.of("java/lang/Integer.class")); + try (JarFile jarFile = new JarFile(filteredJar.toFile())) { + assertThat(jarFile.stream()) + .filteredOn(jarEntry -> jarEntry.getName().equals(JarFile.MANIFEST_NAME)) + .isNotEmpty() + .allMatch(jarEntry -> jarEntry.getTime() == 0); + // Check that the manifest is still has attributes + Manifest manifest = jarFile.getManifest(); + assertThat(manifest.getMainAttributes()).isNotEmpty(); + } + } + + private static KeyStore.PrivateKeyEntry createPrivateKeyEntry() + throws NoSuchAlgorithmException, CertificateException, OperatorCreationException, CertIOException { + KeyPairGenerator ky = KeyPairGenerator.getInstance("RSA"); + ky.initialize(2048); + KeyPair keyPair = ky.generateKeyPair(); + Certificate[] chain = { createCertificate(keyPair, "cn=Unknown") }; + KeyStore.PrivateKeyEntry keyEntry = new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), chain); + return keyEntry; + } + + private static Certificate createCertificate(KeyPair keyPair, String subjectDN) + throws OperatorCreationException, CertificateException, CertIOException { + Provider bcProvider = new BouncyCastleProvider(); + Security.addProvider(bcProvider); + long now = System.currentTimeMillis(); + X500Name dnName = new X500Name(subjectDN); + BigInteger certSerialNumber = new BigInteger(Long.toString(now)); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.YEAR, 1); + Date endDate = calendar.getTime(); + String signatureAlgorithm = "SHA256WithRSA"; + + ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate()); + + JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dnName, certSerialNumber, new Date(now), + endDate, + dnName, keyPair.getPublic()); + + BasicConstraints basicConstraints = new BasicConstraints(true); + + certBuilder.addExtension(new ASN1ObjectIdentifier("2.5.29.19"), true, basicConstraints); + return new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(contentSigner)); + + } + } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/types/TypeParserTest.java b/core/deployment/src/test/java/io/quarkus/deployment/types/TypeParserTest.java new file mode 100644 index 00000000000000..b335c674d88890 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/types/TypeParserTest.java @@ -0,0 +1,157 @@ +package io.quarkus.deployment.types; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import jakarta.enterprise.util.TypeLiteral; + +import org.junit.jupiter.api.Test; + +public class TypeParserTest { + @Test + public void testVoid() { + assertCorrect("void", void.class); + assertCorrect(" void", void.class); + assertCorrect("void ", void.class); + assertCorrect(" void ", void.class); + } + + @Test + public void testPrimitive() { + assertCorrect("boolean", boolean.class); + assertCorrect(" byte", byte.class); + assertCorrect("short ", short.class); + assertCorrect(" int ", int.class); + assertCorrect("\tlong", long.class); + assertCorrect("float\t", float.class); + assertCorrect("\tdouble\t", double.class); + assertCorrect(" \n char \n ", char.class); + } + + @Test + public void testPrimitiveArray() { + assertCorrect("boolean[]", boolean[].class); + assertCorrect("byte [][]", byte[][].class); + assertCorrect("short [] [] []", short[][][].class); + assertCorrect("int [ ] [ ] [ ] [ ]", int[][][][].class); + assertCorrect("long [][][]", long[][][].class); + assertCorrect(" float[][]", float[][].class); + assertCorrect(" double [] ", double[].class); + assertCorrect(" char [ ][ ] ", char[][].class); + } + + @Test + public void testClass() { + assertCorrect("java.lang.Object", Object.class); + assertCorrect("java.lang.String", String.class); + + assertCorrect(" java.lang.Boolean", Boolean.class); + assertCorrect("java.lang.Byte ", Byte.class); + assertCorrect(" java.lang.Short ", Short.class); + assertCorrect("\tjava.lang.Integer", Integer.class); + assertCorrect("java.lang.Long\t", Long.class); + assertCorrect("\tjava.lang.Float\t", Float.class); + assertCorrect(" java.lang.Double", Double.class); + assertCorrect("java.lang.Character ", Character.class); + } + + @Test + public void testClassArray() { + assertCorrect("java.lang.Object[]", Object[].class); + assertCorrect("java.lang.String[][]", String[][].class); + + assertCorrect("java.lang.Boolean[][][]", Boolean[][][].class); + assertCorrect("java.lang.Byte[][][][]", Byte[][][][].class); + assertCorrect("java.lang.Short[][][]", Short[][][].class); + assertCorrect("java.lang.Integer[][]", Integer[][].class); + assertCorrect("java.lang.Long[]", Long[].class); + assertCorrect("java.lang.Float[][]", Float[][].class); + assertCorrect("java.lang.Double[][][]", Double[][][].class); + assertCorrect("java.lang.Character[][][][]", Character[][][][].class); + } + + @Test + public void testParameterizedType() { + assertCorrect("java.util.List", new TypeLiteral>() { + }.getType()); + assertCorrect("java.util.Map", new TypeLiteral>() { + }.getType()); + + assertCorrect("java.util.List", new TypeLiteral>() { + }.getType()); + assertCorrect("java.util.Map>", new TypeLiteral>>() { + }.getType()); + } + + @Test + public void testParameterizedTypeArray() { + assertCorrect("java.util.List[]", new TypeLiteral[]>() { + }.getType()); + assertCorrect("java.util.Map[][]", new TypeLiteral[][]>() { + }.getType()); + } + + @Test + public void testIncorrect() { + assertIncorrect(""); + assertIncorrect(" "); + assertIncorrect("\t"); + assertIncorrect(" "); + assertIncorrect(" \n "); + + assertIncorrect("."); + assertIncorrect(","); + assertIncorrect("["); + assertIncorrect("]"); + assertIncorrect("<"); + assertIncorrect(">"); + + assertIncorrect("int."); + assertIncorrect("int,"); + assertIncorrect("int["); + assertIncorrect("int]"); + assertIncorrect("int[[]"); + assertIncorrect("int[]["); + assertIncorrect("int[]]"); + assertIncorrect("int[0]"); + assertIncorrect("int<"); + assertIncorrect("int>"); + assertIncorrect("int<>"); + + assertIncorrect("java.util.List<"); + assertIncorrect("java.util.List<>"); + assertIncorrect("java.util.List>"); + assertIncorrect("java.util.List"); + assertIncorrect("java.util.List>>"); + + assertIncorrect("java.util.List"); + assertIncorrect("java.util.Map"); + + assertIncorrect("java.lang.Integer."); + assertIncorrect("java .lang.Integer"); + assertIncorrect("java. lang.Integer"); + assertIncorrect("java . lang.Integer"); + assertIncorrect(".java.lang.Integer"); + assertIncorrect(".java.lang.Integer."); + + assertIncorrect("java.lang.Integer["); + assertIncorrect("java.lang.Integer[[]"); + assertIncorrect("java.lang.Integer[]["); + assertIncorrect("java.lang.Integer[]]"); + assertIncorrect("java.lang.Integer[0]"); + } + + private void assertCorrect(String str, Type expectedType) { + assertEquals(expectedType, TypeParser.parse(str)); + } + + private void assertIncorrect(String str) { + assertThrows(IllegalArgumentException.class, () -> TypeParser.parse(str)); + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java b/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java index 518a550c787a7a..d92f7d07bda51d 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java @@ -16,7 +16,7 @@ import org.jboss.jandex.Type.Kind; import org.junit.jupiter.api.Test; -import io.quarkus.commons.classloading.ClassloadHelper; +import io.quarkus.commons.classloading.ClassLoaderHelper; public class JandexUtilTest { @@ -309,7 +309,7 @@ private static Index index(Class... classes) { for (Class clazz : classes) { try { try (InputStream stream = JandexUtilTest.class.getClassLoader() - .getResourceAsStream(ClassloadHelper.fromClassNameToResourceName(clazz.getName()))) { + .getResourceAsStream(ClassLoaderHelper.fromClassNameToResourceName(clazz.getName()))) { indexer.index(stream); } } catch (IOException e) { diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java index 37d963e37ea35e..efbb48dc155850 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java @@ -55,7 +55,12 @@ public static synchronized void waitForApplicationStart() { } } if (startupProblem != null) { - throw new ApplicationStartException(startupProblem); + // let's not keep the startupProblem around in the static field + // as it keeps a reference to the Application in the backtrace + Throwable localStartupProblem = startupProblem; + startupProblem = null; + + throw new ApplicationStartException(localStartupProblem); } } diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/spi/HotReplacementContext.java b/core/devmode-spi/src/main/java/io/quarkus/dev/spi/HotReplacementContext.java index 15f3b0163a960c..aba0ea21e75a08 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/spi/HotReplacementContext.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/spi/HotReplacementContext.java @@ -8,7 +8,7 @@ public interface HotReplacementContext { - Path getClassesDir(); + List getClassesDir(); List getSourcesDir(); diff --git a/core/launcher/src/main/java/io/quarkus/launcher/JBangIntegration.java b/core/launcher/src/main/java/io/quarkus/launcher/JBangIntegration.java index 7e7cf1fb12cb20..e0adc8e68e543b 100644 --- a/core/launcher/src/main/java/io/quarkus/launcher/JBangIntegration.java +++ b/core/launcher/src/main/java/io/quarkus/launcher/JBangIntegration.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Properties; import io.quarkus.bootstrap.BootstrapConstants; @@ -46,6 +47,7 @@ public static Map postBuild(Path appClasses, Path pomFile, List< return ret; } + Properties configurationProperties = new Properties(); for (String comment : comments) { //we allow config to be provided via //Q:CONFIG name=value if (comment.startsWith(CONFIG)) { @@ -54,7 +56,7 @@ public static Map postBuild(Path appClasses, Path pomFile, List< if (equals == -1) { throw new RuntimeException("invalid config " + comment); } - System.setProperty(conf.substring(0, equals), conf.substring(equals + 1)); + configurationProperties.setProperty(conf.substring(0, equals), conf.substring(equals + 1)); } } @@ -117,12 +119,15 @@ public Enumeration getResources(String name) throws IOException { Thread.currentThread().setContextClassLoader(loader); Class launcher = loader.loadClass("io.quarkus.bootstrap.jbang.JBangBuilderImpl"); return (Map) launcher - .getDeclaredMethod("postBuild", Path.class, Path.class, List.class, List.class, boolean.class).invoke( + .getDeclaredMethod("postBuild", Path.class, Path.class, List.class, List.class, Properties.class, + boolean.class) + .invoke( null, appClasses, pomFile, repositories, dependencies, + configurationProperties, nativeImage); } catch (Exception e) { throw new RuntimeException(e); diff --git a/core/processor/pom.xml b/core/processor/pom.xml index aee2ce4090171d..44d6366b1499b9 100644 --- a/core/processor/pom.xml +++ b/core/processor/pom.xml @@ -32,6 +32,14 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.module + jackson-module-parameter-names + io.quarkus quarkus-bootstrap-app-model @@ -62,7 +70,7 @@ com.karuslabs elementary - 2.0.1 + 3.0.0 test @@ -80,6 +88,10 @@ -proc:none + + org.jboss.bridger + bridger + diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java b/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java deleted file mode 100644 index 6418a4128edb8c..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java +++ /dev/null @@ -1,141 +0,0 @@ -package io.quarkus.annotation.processor; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; -import java.util.Properties; -import java.util.Set; -import java.util.regex.Pattern; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.quarkus.annotation.processor.generate_doc.ConfigDocItem; - -final public class Constants { - public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - public static TypeReference> LIST_OF_CONFIG_ITEMS_TYPE_REF = new TypeReference<>() { - }; - - public static final char DOT = '.'; - public static final String CODE_DELIMITER = "`"; - public static final String EMPTY = ""; - public static final String DASH = "-"; - public static final String ADOC_EXTENSION = ".adoc"; - public static final String DIGIT_OR_LOWERCASE = "^[a-z0-9]+$"; - public static final String NEW_LINE = "\n"; - public static final String SECTION_TITLE_L1 = "= "; - - public static final String PARENT = "<>"; - public static final String NO_DEFAULT = "<>"; - public static final String HYPHENATED_ELEMENT_NAME = "<>"; - - public static final String COMMON = "common"; - public static final String RUNTIME = "runtime"; - public static final String DEPLOYMENT = "deployment"; - public static final String CONFIG = "config"; - - public static final Pattern CLASS_NAME_PATTERN = Pattern.compile("^.+[\\.$](\\w+)$"); - public static final Pattern PKG_PATTERN = Pattern - .compile("^io\\.quarkus\\.(\\w+)\\.?(\\w+)?\\.?(\\w+)?\\.?(\\w+)?\\.?(\\w+)?"); - - public static final String INSTANCE_SYM = "__instance"; - public static final String QUARKUS = "quarkus"; - - public static final String ANNOTATION_RECORDER = "io.quarkus.runtime.annotations.Recorder"; - public static final String ANNOTATION_RECORD = "io.quarkus.deployment.annotations.Record"; - - public static final String MEMORY_SIZE_TYPE = "io.quarkus.runtime.configuration.MemorySize"; - public static final String ANNOTATION_CONFIG_ITEM = "io.quarkus.runtime.annotations.ConfigItem"; - public static final String ANNOTATION_BUILD_STEP = "io.quarkus.deployment.annotations.BuildStep"; - public static final String ANNOTATION_CONFIG_ROOT = "io.quarkus.runtime.annotations.ConfigRoot"; - public static final String ANNOTATION_CONFIG_MAPPING = "io.smallrye.config.ConfigMapping"; - public static final String ANNOTATION_DEFAULT_CONVERTER = "io.quarkus.runtime.annotations.DefaultConverter"; - public static final String ANNOTATION_CONVERT_WITH = "io.quarkus.runtime.annotations.ConvertWith"; - public static final String ANNOTATION_CONFIG_GROUP = "io.quarkus.runtime.annotations.ConfigGroup"; - public static final String ANNOTATION_CONFIG_DOC_IGNORE = "io.quarkus.runtime.annotations.ConfigDocIgnore"; - public static final String ANNOTATION_CONFIG_DOC_MAP_KEY = "io.quarkus.runtime.annotations.ConfigDocMapKey"; - public static final String ANNOTATION_CONFIG_DOC_SECTION = "io.quarkus.runtime.annotations.ConfigDocSection"; - public static final String ANNOTATION_CONFIG_DOC_ENUM_VALUE = "io.quarkus.runtime.annotations.ConfigDocEnumValue"; - public static final String ANNOTATION_CONFIG_DOC_DEFAULT = "io.quarkus.runtime.annotations.ConfigDocDefault"; - public static final String ANNOTATION_CONFIG_DOC_FILE_NAME = "io.quarkus.runtime.annotations.ConfigDocFilename"; - - public static final String ANNOTATION_CONFIG_WITH_NAME = "io.smallrye.config.WithName"; - public static final String ANNOTATION_CONFIG_WITH_PARENT_NAME = "io.smallrye.config.WithParentName"; - public static final String ANNOTATION_CONFIG_WITH_DEFAULT = "io.smallrye.config.WithDefault"; - public static final String ANNOTATION_CONFIG_WITH_UNNAMED_KEY = "io.smallrye.config.WithUnnamedKey"; - - public static final Set SUPPORTED_ANNOTATIONS_TYPES = Set.of(ANNOTATION_BUILD_STEP, ANNOTATION_CONFIG_GROUP, - ANNOTATION_CONFIG_ROOT, ANNOTATION_RECORDER, ANNOTATION_CONFIG_MAPPING); - - public static final Map ALIASED_TYPES = Map.of( - OptionalLong.class.getName(), Long.class.getName(), - OptionalInt.class.getName(), Integer.class.getName(), - OptionalDouble.class.getName(), Double.class.getName(), - "java.lang.Class", "class name", - "java.net.InetSocketAddress", "host:port", - Path.class.getName(), "path", - String.class.getName(), "string"); - - private static final Properties SYSTEM_PROPERTIES = System.getProperties(); - - private static final String DOCS_SRC_MAIN_ASCIIDOC_GENERATED = "/target/asciidoc/generated/config/"; - private static final String DOCS_OUT_DIR = System.getProperty("quarkus.docsOutputDir", - SYSTEM_PROPERTIES.getProperty("maven.multiModuleProjectDirectory", ".")); - public static final Path GENERATED_DOCS_PATH = Paths.get(DOCS_OUT_DIR + DOCS_SRC_MAIN_ASCIIDOC_GENERATED).toAbsolutePath(); - public static final String SUMMARY_TABLE_ID_VARIABLE = "summaryTableId"; - public static final String DURATION_NOTE_ANCHOR = String.format("duration-note-anchor-{%s}", - SUMMARY_TABLE_ID_VARIABLE); - public static final String MEMORY_SIZE_NOTE_ANCHOR = "memory-size-note-anchor"; - public static final String MORE_INFO_ABOUT_TYPE_FORMAT = " link:#%s[icon:question-circle[title=More information about the %s format]]"; - public static final String DURATION_INFORMATION = String.format(Constants.MORE_INFO_ABOUT_TYPE_FORMAT, - Constants.DURATION_NOTE_ANCHOR, Duration.class.getSimpleName()); - public static final String MEMORY_SIZE_INFORMATION = String.format(Constants.MORE_INFO_ABOUT_TYPE_FORMAT, - Constants.MEMORY_SIZE_NOTE_ANCHOR, "MemorySize"); - - public static final String CONFIG_PHASE_BUILD_TIME_ILLUSTRATION = "icon:lock[title=Fixed at build time]"; - public static final String CONFIG_PHASE_LEGEND = String.format( - "%n%s Configuration property fixed at build time - All other configuration properties are overridable at runtime", - CONFIG_PHASE_BUILD_TIME_ILLUSTRATION); - - public static final String DURATION_FORMAT_NOTE = "\nifndef::no-duration-note[]\n[NOTE]" + - "\n[id='" + DURATION_NOTE_ANCHOR + "']\n" + - ".About the Duration format\n" + - "====\n" + - "To write duration values, use the standard `java.time.Duration` format.\n" + - "See the link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information.\n" - + - "\n" + - "You can also use a simplified format, starting with a number:\n" + - "\n" + - "* If the value is only a number, it represents time in seconds.\n" + - "* If the value is a number followed by `ms`, it represents time in milliseconds.\n" + - "\n" + - "In other cases, the simplified format is translated to the `java.time.Duration` format for parsing:\n" + - "\n" + - "* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`.\n" + - "* If the value is a number followed by `d`, it is prefixed with `P`" + - ".\n" + - "====\n" + - "endif::no-duration-note[]\n"; - - public static final String MEMORY_SIZE_FORMAT_NOTE = "\n[NOTE]" + - "\n[[" + MEMORY_SIZE_NOTE_ANCHOR + "]]\n" + - ".About the MemorySize format\n" + - "====\n" + - "A size configuration option recognises string in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`.\n" - + - "If no suffix is given, assume bytes.\n" + - "====\n"; - - /** - * Tooltip is custom AsciiDoc inline macro that transforms inputs to a CSS Tooltip. - */ - public static final String TOOLTIP = "tooltip:%s[%s]"; - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java index 3d978555bbecc0..c224ba30c764b4 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java @@ -1,1027 +1,112 @@ package io.quarkus.annotation.processor; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_GROUP; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_MAPPING; -import static javax.lang.model.util.ElementFilter.constructorsIn; -import static javax.lang.model.util.ElementFilter.fieldsIn; -import static javax.lang.model.util.ElementFilter.methodsIn; -import static javax.lang.model.util.ElementFilter.typesIn; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.FileVisitor; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Arrays; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Completion; -import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedOptions; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.Name; -import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; -import javax.tools.FileObject; -import javax.tools.StandardLocation; +import javax.tools.Diagnostic.Kind; -import org.jboss.jdeparser.FormatPreferences; -import org.jboss.jdeparser.JAssignableExpr; -import org.jboss.jdeparser.JCall; -import org.jboss.jdeparser.JClassDef; import org.jboss.jdeparser.JDeparser; -import org.jboss.jdeparser.JExprs; -import org.jboss.jdeparser.JFiler; -import org.jboss.jdeparser.JMethodDef; -import org.jboss.jdeparser.JMod; -import org.jboss.jdeparser.JSourceFile; -import org.jboss.jdeparser.JSources; -import org.jboss.jdeparser.JType; -import org.jboss.jdeparser.JTypes; -import io.quarkus.annotation.processor.generate_doc.ConfigDocGeneratedOutput; -import io.quarkus.annotation.processor.generate_doc.ConfigDocItemScanner; -import io.quarkus.annotation.processor.generate_doc.ConfigDocWriter; -import io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil; -import io.quarkus.bootstrap.util.PropertyUtils; +import io.quarkus.annotation.processor.documentation.config.ConfigDocExtensionProcessor; +import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.extension.ExtensionBuildProcessor; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; +@SupportedOptions({ Options.LEGACY_CONFIG_ROOT, Options.GENERATE_DOC }) public class ExtensionAnnotationProcessor extends AbstractProcessor { - private static final Pattern REMOVE_LEADING_SPACE = Pattern.compile("^ ", Pattern.MULTILINE); - private static final String QUARKUS_GENERATED = "io.quarkus.Generated"; - - private final ConfigDocWriter configDocWriter = new ConfigDocWriter(); - private final ConfigDocItemScanner configDocItemScanner = new ConfigDocItemScanner(); - private final Set generatedAccessors = new ConcurrentHashMap().keySet(Boolean.TRUE); - private final Set generatedJavaDocs = new ConcurrentHashMap().keySet(Boolean.TRUE); - private final boolean generateDocs = !(Boolean.getBoolean("skipDocs") || Boolean.getBoolean("quickly")); - - private final Map ANNOTATION_USAGE_TRACKER = new ConcurrentHashMap<>(); - - public ExtensionAnnotationProcessor() { - } - - @Override - public Set getSupportedOptions() { - return Collections.emptySet(); - } - - @Override - public Set getSupportedAnnotationTypes() { - return Constants.SUPPORTED_ANNOTATIONS_TYPES; - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latest(); - } + private static final String DEBUG = "debug-extension-annotation-processor"; - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - try { - doProcess(annotations, roundEnv); - if (roundEnv.processingOver()) { - doFinish(); - } - return true; - } finally { - JDeparser.dropCaches(); - } - } + private Utils utils; + private List extensionProcessors; @Override - public Iterable getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, - String userText) { - return Collections.emptySet(); - } - - public void doProcess(Set annotations, RoundEnvironment roundEnv) { - for (TypeElement annotation : annotations) { - switch (annotation.getQualifiedName() - .toString()) { - case Constants.ANNOTATION_BUILD_STEP: - trackAnnotationUsed(Constants.ANNOTATION_BUILD_STEP); - processBuildStep(roundEnv, annotation); - break; - case Constants.ANNOTATION_CONFIG_GROUP: - trackAnnotationUsed(Constants.ANNOTATION_CONFIG_GROUP); - processConfigGroup(roundEnv, annotation); - break; - case Constants.ANNOTATION_CONFIG_ROOT: - trackAnnotationUsed(Constants.ANNOTATION_CONFIG_ROOT); - processConfigRoot(roundEnv, annotation); - break; - case Constants.ANNOTATION_RECORDER: - trackAnnotationUsed(Constants.ANNOTATION_RECORDER); - processRecorder(roundEnv, annotation); - break; - case Constants.ANNOTATION_CONFIG_MAPPING: - trackAnnotationUsed(Constants.ANNOTATION_CONFIG_MAPPING); - break; - } - } - } - - void doFinish() { - validateAnnotationUsage(); - - final Filer filer = processingEnv.getFiler(); - final FileObject tempResource; - try { - tempResource = filer.createResource(StandardLocation.SOURCE_OUTPUT, Constants.EMPTY, "ignore.tmp"); - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Unable to create temp output file: " + e); - return; - } - final URI uri = tempResource.toUri(); - // tempResource.delete(); - Path path; - try { - path = Paths.get(uri) - .getParent(); - } catch (RuntimeException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Resource path URI is invalid: " + uri); - return; - } - Collection bscListClasses = new TreeSet<>(); - Collection crListClasses = new TreeSet<>(); - Properties javaDocProperties = new Properties(); - - try { - Files.walkFileTree(path, new FileVisitor<>() { - public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) { - return FileVisitResult.CONTINUE; - } - - public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) { - final String nameStr = file.getFileName() - .toString(); - if (nameStr.endsWith(".bsc")) { - readFile(file, bscListClasses); - } else if (nameStr.endsWith(".cr")) { - readFile(file, crListClasses); - } else if (nameStr.endsWith(".jdp")) { - final Properties p = new Properties(); - try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { - p.load(br); - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Failed to read file " + file + ": " + e); - } - final Set names = p.stringPropertyNames(); - for (String name : names) { - javaDocProperties.setProperty(name, p.getProperty(name)); - } - } - - return FileVisitResult.CONTINUE; - } - - public FileVisitResult visitFileFailed(final Path file, final IOException exc) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Failed to visit file " + file + ": " + exc); - return FileVisitResult.CONTINUE; - } - - public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) { - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "File walk failed: " + e); - } - if (!bscListClasses.isEmpty()) - try { - final FileObject listResource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", - "META-INF/quarkus-build-steps.list"); - writeListResourceFile(bscListClasses, listResource); - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Failed to write build steps listing: " + e); - return; - } - if (!crListClasses.isEmpty()) { - try { - final FileObject listResource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", - "META-INF/quarkus-config-roots.list"); - writeListResourceFile(crListClasses, listResource); - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Failed to write config roots listing: " + e); - return; - } - } - - if (!javaDocProperties.isEmpty()) { - try { - final FileObject listResource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", - "META-INF/quarkus-javadoc.properties"); - try (OutputStream os = listResource.openOutputStream()) { - try (BufferedOutputStream bos = new BufferedOutputStream(os)) { - try (OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) { - try (BufferedWriter bw = new BufferedWriter(osw)) { - PropertyUtils.store(javaDocProperties, bw); - } - } - } - } - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Failed to write javadoc properties: " + e); - return; - } - } + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); - try { - if (generateDocs) { - final Set outputs = configDocItemScanner - .scanExtensionsConfigurationItems(javaDocProperties, isAnnotationUsed(ANNOTATION_CONFIG_MAPPING)); - for (ConfigDocGeneratedOutput output : outputs) { - DocGeneratorUtil.sort(output.getConfigDocItems()); // sort before writing - configDocWriter.writeAllExtensionConfigDocumentation(output); - } - } - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Failed to generate extension doc: " + e); - } - } - - private void validateAnnotationUsage() { - if (isAnnotationUsed(Constants.ANNOTATION_BUILD_STEP) && isAnnotationUsed(Constants.ANNOTATION_RECORDER)) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Detected use of @Recorder annotation in 'deployment' module. Classes annotated with @Recorder must be " - + - "part of the extension's 'runtime' module"); - } - } - - private boolean isAnnotationUsed(String annotation) { - return ANNOTATION_USAGE_TRACKER.getOrDefault(annotation, false); - } - - private void trackAnnotationUsed(String annotation) { - ANNOTATION_USAGE_TRACKER.put(annotation, true); - } - - private void writeListResourceFile(Collection crListClasses, FileObject listResource) throws IOException { - try (OutputStream os = listResource.openOutputStream()) { - try (BufferedOutputStream bos = new BufferedOutputStream(os)) { - try (OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) { - try (BufferedWriter bw = new BufferedWriter(osw)) { - for (String item : crListClasses) { - bw.write(item); - bw.newLine(); - } - } - } - } - } - } - - private void readFile(Path file, Collection bscListClasses) { - try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty()) { - bscListClasses.add(line); - } - } - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Failed to read file " + file + ": " + e); - } - } + utils = new Utils(processingEnv); - private void processBuildStep(RoundEnvironment roundEnv, TypeElement annotation) { - final Set processorClassNames = new HashSet<>(); + boolean useConfigMapping = !Boolean + .parseBoolean(utils.processingEnv().getOptions().getOrDefault(Options.LEGACY_CONFIG_ROOT, "false")); + boolean debug = Boolean.getBoolean(DEBUG); - for (ExecutableElement i : methodsIn(roundEnv.getElementsAnnotatedWith(annotation))) { - final TypeElement clazz = getClassOf(i); - if (clazz == null) { - continue; - } + Extension extension = utils.extension().getExtension(); + Config config = new Config(extension, useConfigMapping, debug); - final PackageElement pkg = processingEnv.getElementUtils() - .getPackageOf(clazz); - if (pkg == null) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Element " + clazz + " has no enclosing package"); - continue; - } - - final String binaryName = processingEnv.getElementUtils() - .getBinaryName(clazz) - .toString(); - if (processorClassNames.add(binaryName)) { - validateRecordBuildSteps(clazz); - recordConfigJavadoc(clazz); - generateAccessor(clazz); - final StringBuilder rbn = getRelativeBinaryName(clazz, new StringBuilder()); - try { - final FileObject itemResource = processingEnv.getFiler() - .createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName() - .toString(), - rbn + ".bsc", clazz); - writeResourceFile(binaryName, itemResource); - } catch (IOException e1) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); - } - } + if (!useConfigMapping) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Extension " + extension.artifactId() + + " config implementation is deprecated. Please migrate to use @ConfigMapping: https://quarkus.io/guides/writing-extensions#configuration"); } - } - private void validateRecordBuildSteps(TypeElement clazz) { - for (Element e : clazz.getEnclosedElements()) { - if (e.getKind() != ElementKind.METHOD) { - continue; - } - ExecutableElement ex = (ExecutableElement) e; - if (!isAnnotationPresent(ex, Constants.ANNOTATION_BUILD_STEP)) { - continue; - } - if (!isAnnotationPresent(ex, Constants.ANNOTATION_RECORD)) { - continue; - } + List extensionProcessors = new ArrayList<>(); + extensionProcessors.add(new ExtensionBuildProcessor()); - boolean hasRecorder = false; - boolean allTypesResolvable = true; - for (VariableElement parameter : ex.getParameters()) { - String parameterClassName = parameter.asType() - .toString(); - TypeElement parameterTypeElement = processingEnv.getElementUtils() - .getTypeElement(parameterClassName); - if (parameterTypeElement == null) { - allTypesResolvable = false; - } else { - if (isAnnotationPresent(parameterTypeElement, Constants.ANNOTATION_RECORDER)) { - if (parameterTypeElement.getModifiers() - .contains(Modifier.FINAL)) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Class '" + parameterTypeElement.getQualifiedName() - + "' is annotated with @Recorder and therefore cannot be made as a final class."); - } else if (getPackageName(clazz).equals(getPackageName(parameterTypeElement))) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.WARNING, - "Build step class '" + clazz.getQualifiedName() - + "' and recorder '" + parameterTypeElement - + "' share the same package. This is highly discouraged as it can lead to " - + - "unexpected results."); - } - hasRecorder = true; - break; - } - } - } + boolean skipDocs = Boolean.getBoolean("skipDocs") || Boolean.getBoolean("quickly"); + boolean generateDoc = !skipDocs && !"false".equals(processingEnv.getOptions().get(Options.GENERATE_DOC)); - if (!hasRecorder && allTypesResolvable) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Build Step '" + clazz.getQualifiedName() + "#" - + ex.getSimpleName() - + "' which is annotated with '@Record' does not contain a method parameter whose type is annotated " - + - "with '@Recorder'."); + // for now, we generate the old config doc by default but we will change this behavior soon + if (generateDoc) { + if (extension.detected()) { + extensionProcessors.add(new ConfigDocExtensionProcessor()); + } else { + processingEnv.getMessager().printMessage(Kind.WARNING, + "We could not detect the groupId and artifactId of this module (maybe you are using Gradle to build your extension?). The generation of the configuration documentation has been disabled."); } } - } - private Name getPackageName(TypeElement clazz) { - return processingEnv.getElementUtils() - .getPackageOf(clazz) - .getQualifiedName(); - } - - private StringBuilder getRelativeBinaryName(TypeElement te, StringBuilder b) { - final Element enclosing = te.getEnclosingElement(); - if (enclosing instanceof TypeElement) { - getRelativeBinaryName((TypeElement) enclosing, b); - b.append('$'); - } - b.append(te.getSimpleName()); - return b; - } + this.extensionProcessors = Collections.unmodifiableList(extensionProcessors); - private TypeElement getClassOf(Element e) { - Element t = e; - while (!(t instanceof TypeElement)) { - if (t == null) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Element " + e + " has no enclosing class"); - return null; - } - t = t.getEnclosingElement(); - } - return (TypeElement) t; - } - - private void recordConfigJavadoc(TypeElement clazz) { - String className = clazz.getQualifiedName() - .toString(); - if (!generatedJavaDocs.add(className)) - return; - Properties javadocProps = new Properties(); - for (Element e : clazz.getEnclosedElements()) { - switch (e.getKind()) { - case FIELD: { - if (isDocumentedConfigItem(e)) { - processFieldConfigItem((VariableElement) e, javadocProps, className); - } - break; - } - case CONSTRUCTOR: { - final ExecutableElement ex = (ExecutableElement) e; - if (hasParameterDocumentedConfigItem(ex)) { - processCtorConfigItem(ex, javadocProps, className); - } - break; - } - case METHOD: { - final ExecutableElement ex = (ExecutableElement) e; - if (hasParameterDocumentedConfigItem(ex)) { - processMethodConfigItem(ex, javadocProps, className); - } - break; - } - case ENUM: - e - .getEnclosedElements() - .stream() - .filter(e1 -> e1.getKind() == ElementKind.ENUM_CONSTANT) - .forEach(ec -> processEnumConstant(ec, javadocProps, className)); - break; - default: - } + for (ExtensionProcessor extensionProcessor : this.extensionProcessors) { + extensionProcessor.init(config, utils); } - writeJavadocProperties(clazz, javadocProps); } - private void recordMappingJavadoc(TypeElement clazz) { - String className = clazz.getQualifiedName() - .toString(); - if (!generatedJavaDocs.add(className)) - return; - if (!isAnnotationPresent(clazz, ANNOTATION_CONFIG_MAPPING)) { - if (generateDocs) { - configDocItemScanner.addConfigGroups(clazz); - } - } - Properties javadocProps = new Properties(); - recordMappingJavadoc(clazz, javadocProps); - writeJavadocProperties(clazz, javadocProps); + @Override + public Set getSupportedAnnotationTypes() { + return Types.SUPPORTED_ANNOTATIONS_TYPES; } - private void recordMappingJavadoc(final TypeElement clazz, final Properties javadocProps) { - String className = clazz.getQualifiedName() - .toString(); - for (Element e : clazz.getEnclosedElements()) { - switch (e.getKind()) { - case INTERFACE: { - recordMappingJavadoc(((TypeElement) e)); - break; - } - - case METHOD: { - if (!isConfigMappingMethodIgnored(e)) { - processMethodConfigMapping((ExecutableElement) e, javadocProps, className); - } - break; - } - default: - } - } + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); } - private boolean isEnclosedByMapping(Element clazz) { - if (clazz.getKind() - .equals(ElementKind.INTERFACE)) { - Element enclosingElement = clazz.getEnclosingElement(); - if (enclosingElement.getKind() - .equals(ElementKind.INTERFACE)) { - if (isAnnotationPresent(enclosingElement, ANNOTATION_CONFIG_MAPPING)) { - return true; - } else { - isEnclosedByMapping(enclosingElement); - } - } - } - return false; + @Override + public Iterable getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, + String userText) { + return Collections.emptySet(); } - private void writeJavadocProperties(final TypeElement clazz, final Properties javadocProps) { - if (javadocProps.isEmpty()) - return; - final PackageElement pkg = processingEnv.getElementUtils() - .getPackageOf(clazz); - final String rbn = getRelativeBinaryName(clazz, new StringBuilder()).append(".jdp") - .toString(); + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { try { - FileObject file = processingEnv.getFiler() - .createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName() - .toString(), - rbn, - clazz); - try (Writer writer = file.openWriter()) { - PropertyUtils.store(javadocProps, writer); - } - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Failed to persist resource " + rbn + ": " + e); - } - } - - private void processFieldConfigItem(VariableElement field, Properties javadocProps, String className) { - javadocProps.put(className + Constants.DOT + field.getSimpleName() - .toString(), getRequiredJavadoc(field)); - } - - private void processEnumConstant(Element field, Properties javadocProps, String className) { - String javaDoc = getJavadoc(field); - if (javaDoc != null && !javaDoc.isBlank()) { - javadocProps.put(className + Constants.DOT + field.getSimpleName() - .toString(), javaDoc); - } - } - - private void processCtorConfigItem(ExecutableElement ctor, Properties javadocProps, String className) { - final String docComment = getRequiredJavadoc(ctor); - final StringBuilder buf = new StringBuilder(); - appendParamTypes(ctor, buf); - javadocProps.put(className + Constants.DOT + buf, docComment); - } - - private void processMethodConfigItem(ExecutableElement method, Properties javadocProps, String className) { - final String docComment = getRequiredJavadoc(method); - final StringBuilder buf = new StringBuilder(); - buf.append(method.getSimpleName() - .toString()); - appendParamTypes(method, buf); - javadocProps.put(className + Constants.DOT + buf, docComment); - } - - private void processMethodConfigMapping(ExecutableElement method, Properties javadocProps, String className) { - if (method.getModifiers() - .contains(Modifier.ABSTRACT)) { - // Skip toString method, because mappings can include it and generate it - if (method.getSimpleName() - .contentEquals("toString") - && method.getParameters() - .isEmpty()) { - return; - } - - String docComment = getRequiredJavadoc(method); - javadocProps.put(className + Constants.DOT + method.getSimpleName() - .toString(), docComment); - - // Find groups without annotation - TypeMirror returnType = method.getReturnType(); - if (TypeKind.DECLARED.equals(returnType.getKind())) { - DeclaredType declaredType = (DeclaredType) returnType; - if (!isAnnotationPresent(declaredType.asElement(), ANNOTATION_CONFIG_GROUP)) { - TypeElement type = unwrapConfigGroup(returnType); - if (type != null && ElementKind.INTERFACE.equals(type.getKind())) { - recordMappingJavadoc(type); - configDocItemScanner.addConfigGroups(type); - } - } - } - } - } - - private TypeElement unwrapConfigGroup(TypeMirror typeMirror) { - if (typeMirror == null) { - return null; - } - - DeclaredType declaredType = (DeclaredType) typeMirror; - String name = declaredType.asElement() - .toString(); - List typeArguments = declaredType.getTypeArguments(); - if (typeArguments.isEmpty()) { - if (!name.startsWith("java.")) { - return (TypeElement) declaredType.asElement(); - } - } else if (typeArguments.size() == 1) { - if (name.equals(Optional.class.getName()) || - name.equals(List.class.getName()) || - name.equals(Set.class.getName())) { - return unwrapConfigGroup(typeArguments.get(0)); - } - } else if (typeArguments.size() == 2) { - if (name.equals(Map.class.getName())) { - return unwrapConfigGroup(typeArguments.get(1)); - } - } - return null; - } - - private void processConfigGroup(RoundEnvironment roundEnv, TypeElement annotation) { - final Set groupClassNames = new HashSet<>(); - for (TypeElement i : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - if (groupClassNames.add(i.getQualifiedName() - .toString())) { - generateAccessor(i); - if (isEnclosedByMapping(i) || i.getKind() - .equals(ElementKind.INTERFACE)) { - recordMappingJavadoc(i); - } else { - recordConfigJavadoc(i); - } - if (generateDocs) { - configDocItemScanner.addConfigGroups(i); - } - } - } - } - - private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation) { - final Set rootClassNames = new HashSet<>(); - - for (TypeElement clazz : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - final PackageElement pkg = processingEnv.getElementUtils() - .getPackageOf(clazz); - if (pkg == null) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Element " + clazz + " has no enclosing package"); - continue; - } - - if (generateDocs) { - configDocItemScanner.addConfigRoot(pkg, clazz); - } - - final String binaryName = processingEnv.getElementUtils() - .getBinaryName(clazz) - .toString(); - if (rootClassNames.add(binaryName)) { - // new class - if (isAnnotationPresent(clazz, ANNOTATION_CONFIG_MAPPING)) { - recordMappingJavadoc(clazz); - } else if (isAnnotationPresent(clazz, Constants.ANNOTATION_CONFIG_ROOT)) { - recordConfigJavadoc(clazz); - generateAccessor(clazz); - } - final StringBuilder rbn = getRelativeBinaryName(clazz, new StringBuilder()); - try { - final FileObject itemResource = processingEnv.getFiler() - .createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName() - .toString(), - rbn + ".cr", - clazz); - writeResourceFile(binaryName, itemResource); - } catch (IOException e1) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); - } - } - } - } - - private void writeResourceFile(String binaryName, FileObject itemResource) throws IOException { - try (OutputStream os = itemResource.openOutputStream()) { - try (BufferedOutputStream bos = new BufferedOutputStream(os)) { - try (OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) { - try (BufferedWriter bw = new BufferedWriter(osw)) { - bw.write(binaryName); - bw.newLine(); - } - } - } - } - } - - private void processRecorder(RoundEnvironment roundEnv, TypeElement annotation) { - final Set groupClassNames = new HashSet<>(); - for (TypeElement i : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - if (groupClassNames.add(i.getQualifiedName() - .toString())) { - generateAccessor(i); - recordConfigJavadoc(i); - } - } - } - - private void generateAccessor(final TypeElement clazz) { - if (!generatedAccessors.add(clazz.getQualifiedName() - .toString())) - return; - final FormatPreferences fp = new FormatPreferences(); - final JSources sources = JDeparser.createSources(JFiler.newInstance(processingEnv.getFiler()), fp); - final PackageElement packageElement = processingEnv.getElementUtils() - .getPackageOf(clazz); - final String className = getRelativeBinaryName(clazz, new StringBuilder()).append("$$accessor") - .toString(); - final JSourceFile sourceFile = sources.createSourceFile(packageElement.getQualifiedName() - .toString(), className); - JType clazzType = JTypes.typeOf(clazz.asType()); - if (clazz.asType() instanceof DeclaredType) { - DeclaredType declaredType = ((DeclaredType) clazz.asType()); - TypeMirror enclosingType = declaredType.getEnclosingType(); - if (enclosingType != null && enclosingType.getKind() == TypeKind.DECLARED - && clazz.getModifiers() - .contains(Modifier.STATIC)) { - // Ugly workaround for Eclipse APT and static nested types - clazzType = unnestStaticNestedType(declaredType); - } - } - final JClassDef classDef = sourceFile._class(JMod.PUBLIC | JMod.FINAL, className); - classDef.constructor(JMod.PRIVATE); // no construction - classDef.annotate(QUARKUS_GENERATED) - .value("Quarkus annotation processor"); - final JAssignableExpr instanceName = JExprs.name(Constants.INSTANCE_SYM); - boolean isEnclosingClassPublic = clazz.getModifiers() - .contains(Modifier.PUBLIC); - // iterate fields - boolean generationNeeded = false; - for (VariableElement field : fieldsIn(clazz.getEnclosedElements())) { - final Set mods = field.getModifiers(); - if (mods.contains(Modifier.PRIVATE) || mods.contains(Modifier.STATIC) || mods.contains(Modifier.FINAL)) { - // skip it - continue; - } - final TypeMirror fieldType = field.asType(); - if (mods.contains(Modifier.PUBLIC) && isEnclosingClassPublic) { - // we don't need to generate a method accessor when the following conditions are met: - // 1) the field is public - // 2) the enclosing class is public - // 3) the class type of the field is public - if (fieldType instanceof DeclaredType) { - final DeclaredType declaredType = (DeclaredType) fieldType; - final TypeElement typeElement = (TypeElement) declaredType.asElement(); - if (typeElement.getModifiers() - .contains(Modifier.PUBLIC)) { - continue; - } - } else { - continue; - } - - } - generationNeeded = true; - - final JType realType = JTypes.typeOf(fieldType); - final JType publicType = fieldType instanceof PrimitiveType ? realType : JType.OBJECT; - - final String fieldName = field.getSimpleName() - .toString(); - final JMethodDef getter = classDef.method(JMod.PUBLIC | JMod.STATIC, publicType, "get_" + fieldName); - getter.annotate(SuppressWarnings.class) - .value("unchecked"); - getter.param(JType.OBJECT, Constants.INSTANCE_SYM); - getter.body() - ._return(instanceName.cast(clazzType) - .field(fieldName)); - final JMethodDef setter = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.VOID, "set_" + fieldName); - setter.annotate(SuppressWarnings.class) - .value("unchecked"); - setter.param(JType.OBJECT, Constants.INSTANCE_SYM); - setter.param(publicType, fieldName); - final JAssignableExpr fieldExpr = JExprs.name(fieldName); - setter.body() - .assign(instanceName.cast(clazzType) - .field(fieldName), - (publicType.equals(realType) ? fieldExpr : fieldExpr.cast(realType))); - } - - // we need to generate an accessor if the class isn't public - if (!isEnclosingClassPublic) { - for (ExecutableElement ctor : constructorsIn(clazz.getEnclosedElements())) { - if (ctor.getModifiers() - .contains(Modifier.PRIVATE)) { - // skip it - continue; - } - generationNeeded = true; - StringBuilder b = new StringBuilder(); - for (VariableElement parameter : ctor.getParameters()) { - b.append('_'); - b.append(parameter.asType() - .toString() - .replace('.', '_')); - } - String codedName = b.toString(); - final JMethodDef ctorMethod = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.OBJECT, "construct" + codedName); - final JCall ctorCall = clazzType._new(); - for (VariableElement parameter : ctor.getParameters()) { - final TypeMirror paramType = parameter.asType(); - final JType realType = JTypes.typeOf(paramType); - final JType publicType = paramType instanceof PrimitiveType ? realType : JType.OBJECT; - final String name = parameter.getSimpleName() - .toString(); - ctorMethod.param(publicType, name); - final JAssignableExpr nameExpr = JExprs.name(name); - ctorCall.arg(publicType.equals(realType) ? nameExpr : nameExpr.cast(realType)); - } - ctorMethod.body() - ._return(ctorCall); + for (ExtensionProcessor extensionProcessor : extensionProcessors) { + extensionProcessor.process(annotations, roundEnv); } - } - - // if no constructor or field access is needed, don't generate anything - if (generationNeeded) { - try { - sources.writeSources(); - } catch (IOException e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Failed to generate source file: " + e, clazz); - } - } - } - - private JType unnestStaticNestedType(DeclaredType declaredType) { - final TypeElement typeElement = (TypeElement) declaredType.asElement(); - - final String name = typeElement.getQualifiedName() - .toString(); - final JType rawType = JTypes.typeNamed(name); - final List typeArguments = declaredType.getTypeArguments(); - if (typeArguments.isEmpty()) { - return rawType; - } - JType[] args = new JType[typeArguments.size()]; - for (int i = 0; i < typeArguments.size(); i++) { - final TypeMirror argument = typeArguments.get(i); - args[i] = JTypes.typeOf(argument); - } - return rawType.typeArg(args); - } - - private void appendParamTypes(ExecutableElement ex, final StringBuilder buf) { - final List params = ex.getParameters(); - if (params.isEmpty()) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "Expected at least one parameter", ex); - return; - } - VariableElement param = params.get(0); - DeclaredType dt = (DeclaredType) param.asType(); - String typeName = processingEnv.getElementUtils() - .getBinaryName(((TypeElement) dt.asElement())) - .toString(); - buf.append('(') - .append(typeName); - for (int i = 1; i < params.size(); ++i) { - param = params.get(i); - dt = (DeclaredType) param.asType(); - typeName = processingEnv.getElementUtils() - .getBinaryName(((TypeElement) dt.asElement())) - .toString(); - buf.append(',') - .append(typeName); - } - buf.append(')'); - } - - private String getRequiredJavadoc(Element e) { - String javaDoc = getJavadoc(e); - if (javaDoc == null) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); - return ""; - } - return javaDoc; - } - - private String getJavadoc(Element e) { - String docComment = processingEnv.getElementUtils() - .getDocComment(e); - - if (docComment == null) { - return null; - } - - // javax.lang.model keeps the leading space after the "*" so we need to remove it. - - return REMOVE_LEADING_SPACE.matcher(docComment) - .replaceAll("") - .trim(); - } - - private static boolean isDocumentedConfigItem(Element element) { - boolean hasAnnotation = false; - for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) annotationMirror.getAnnotationType() - .asElement()) - .getQualifiedName() - .toString(); - if (Constants.ANNOTATION_CONFIG_ITEM.equals(annotationName)) { - hasAnnotation = true; - Object generateDocumentation = getAnnotationAttribute(annotationMirror, "generateDocumentation()"); - if (generateDocumentation != null && !(Boolean) generateDocumentation) { - // Documentation is explicitly disabled - return false; + if (roundEnv.processingOver()) { + for (ExtensionProcessor extensionProcessor : extensionProcessors) { + extensionProcessor.finalizeProcessing(); } - } else if (Constants.ANNOTATION_CONFIG_DOC_SECTION.equals(annotationName)) { - hasAnnotation = true; - } - } - return hasAnnotation; - } - - private static boolean isConfigMappingMethodIgnored(Element element) { - for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) annotationMirror.getAnnotationType() - .asElement()) - .getQualifiedName() - .toString(); - if (Constants.ANNOTATION_CONFIG_DOC_IGNORE.equals(annotationName)) { - return true; - } - } - return false; - } - - private static Object getAnnotationAttribute(AnnotationMirror annotationMirror, String attributeName) { - for (Map.Entry entry : annotationMirror - .getElementValues() - .entrySet()) { - final String key = entry.getKey() - .toString(); - final Object value = entry.getValue() - .getValue(); - if (attributeName.equals(key)) { - return value; - } - } - return null; - } - - private static boolean hasParameterDocumentedConfigItem(ExecutableElement ex) { - for (VariableElement param : ex.getParameters()) { - if (isDocumentedConfigItem(param)) { - return true; - } - } - - return false; - } - - private static boolean isAnnotationPresent(Element element, String... annotationNames) { - Set annotations = new HashSet<>(Arrays.asList(annotationNames)); - for (AnnotationMirror i : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) i.getAnnotationType() - .asElement()).getQualifiedName() - .toString(); - if (annotations.contains(annotationName)) { - return true; } + return true; + } finally { + JDeparser.dropCaches(); } - return false; } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionProcessor.java new file mode 100644 index 00000000000000..c9ec532b4e9391 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionProcessor.java @@ -0,0 +1,19 @@ +package io.quarkus.annotation.processor; + +import java.util.Set; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.TypeElement; + +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +public interface ExtensionProcessor { + + void init(Config config, Utils utils); + + void process(Set annotations, RoundEnvironment roundEnv); + + void finalizeProcessing(); + +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/Options.java b/core/processor/src/main/java/io/quarkus/annotation/processor/Options.java new file mode 100644 index 00000000000000..0d11b2104a373a --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/Options.java @@ -0,0 +1,7 @@ +package io.quarkus.annotation.processor; + +public final class Options { + + public static final String LEGACY_CONFIG_ROOT = "legacyConfigRoot"; + public static final String GENERATE_DOC = "generateDoc"; +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/Outputs.java b/core/processor/src/main/java/io/quarkus/annotation/processor/Outputs.java new file mode 100644 index 00000000000000..5a823808ba40dd --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/Outputs.java @@ -0,0 +1,31 @@ +package io.quarkus.annotation.processor; + +/** + * Define the outputs here so that they are clearly identified. + *

+ * DO NOT use Path as we need maximum compatibility with the filer API and the ZipPath API. + */ +public final class Outputs { + + public static final String META_INF_QUARKUS_BUILD_STEPS = "META-INF/quarkus-build-steps.list"; + public static final String META_INF_QUARKUS_CONFIG_ROOTS = "META-INF/quarkus-config-roots.list"; + + private static final String QUARKUS_CONFIG_DOC = "quarkus-config-doc"; + public static final String QUARKUS_CONFIG_DOC_JAVADOC = QUARKUS_CONFIG_DOC + "/quarkus-config-javadoc.yaml"; + public static final String QUARKUS_CONFIG_DOC_MODEL = QUARKUS_CONFIG_DOC + "/quarkus-config-model.yaml"; + + public static final String META_INF_QUARKUS_CONFIG = "META-INF/" + QUARKUS_CONFIG_DOC; + public static final String META_INF_QUARKUS_CONFIG_JAVADOC_JSON = META_INF_QUARKUS_CONFIG + "/quarkus-config-javadoc.json"; + public static final String META_INF_QUARKUS_CONFIG_MODEL_JSON = META_INF_QUARKUS_CONFIG + "/quarkus-config-model.json"; + public static final String META_INF_QUARKUS_CONFIG_JAVADOC_YAML = META_INF_QUARKUS_CONFIG + "/quarkus-config-javadoc.yaml"; + public static final String META_INF_QUARKUS_CONFIG_MODEL_YAML = META_INF_QUARKUS_CONFIG + "/quarkus-config-model.yaml"; + + /** + * Ideally, we should remove this file at some point. + */ + @Deprecated(forRemoval = true) + public static final String META_INF_QUARKUS_JAVADOC = "META-INF/quarkus-javadoc.properties"; + + private Outputs() { + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java new file mode 100644 index 00000000000000..75c1e996e5ebda --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java @@ -0,0 +1,105 @@ +package io.quarkus.annotation.processor.documentation.config; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic.Kind; + +import io.quarkus.annotation.processor.ExtensionProcessor; +import io.quarkus.annotation.processor.Outputs; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.model.ResolvedModel; +import io.quarkus.annotation.processor.documentation.config.resolver.ConfigResolver; +import io.quarkus.annotation.processor.documentation.config.scanner.ConfigAnnotationScanner; +import io.quarkus.annotation.processor.documentation.config.scanner.ConfigCollector; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +public class ConfigDocExtensionProcessor implements ExtensionProcessor { + + private Config config; + private Utils utils; + private ConfigAnnotationScanner configAnnotationScanner; + + @Override + public void init(Config config, Utils utils) { + this.config = config; + this.utils = utils; + this.configAnnotationScanner = new ConfigAnnotationScanner(config, utils); + } + + @Override + public void process(Set annotations, RoundEnvironment roundEnv) { + Optional configGroup = findAnnotation(annotations, Types.ANNOTATION_CONFIG_GROUP); + Optional configRoot = findAnnotation(annotations, Types.ANNOTATION_CONFIG_ROOT); + Optional configMapping = findAnnotation(annotations, Types.ANNOTATION_CONFIG_MAPPING); + + // make sure we scan the groups before the root + if (configGroup.isPresent()) { + configAnnotationScanner.scanConfigGroups(roundEnv, configGroup.get()); + } + if (configRoot.isPresent()) { + configAnnotationScanner.scanConfigRoots(roundEnv, configRoot.get()); + } + if (configMapping.isPresent()) { + configAnnotationScanner.scanConfigMappingsWithoutConfigRoot(roundEnv, configMapping.get()); + } + } + + private Optional findAnnotation(Set annotations, String annotationName) { + for (TypeElement annotation : annotations) { + if (annotationName.equals(annotation.getQualifiedName().toString())) { + return Optional.of(annotation); + } + } + + return Optional.empty(); + } + + @Override + public void finalizeProcessing() { + ConfigCollector configCollector = configAnnotationScanner.finalizeProcessing(); + + Properties javadocProperties = new Properties(); + for (Entry javadocElementEntry : configCollector.getJavadocElements().entrySet()) { + if (javadocElementEntry.getValue().description() == null + || javadocElementEntry.getValue().description().isBlank()) { + continue; + } + + javadocProperties.put(javadocElementEntry.getKey(), javadocElementEntry.getValue().description()); + } + utils.filer().write(Outputs.META_INF_QUARKUS_JAVADOC, javadocProperties); + + ConfigResolver configResolver = new ConfigResolver(config, utils, configCollector); + + // the model is not written in the jar file + JavadocElements javadocElements = configResolver.resolveJavadoc(); + if (!javadocElements.elements().isEmpty()) { + utils.filer().writeModel(Outputs.QUARKUS_CONFIG_DOC_JAVADOC, javadocElements); + } + + ResolvedModel resolvedModel = configResolver.resolveModel(); + if (!resolvedModel.getConfigRoots().isEmpty()) { + Path resolvedModelPath = utils.filer().writeModel(Outputs.QUARKUS_CONFIG_DOC_MODEL, resolvedModel); + + if (config.isDebug()) { + try { + utils.processingEnv().getMessager().printMessage(Kind.NOTE, + "Resolved model:\n\n" + Files.readString(resolvedModelPath)); + } catch (IOException e) { + throw new IllegalStateException("Unable to read the resolved model from: " + resolvedModelPath, e); + } + } + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigGroup.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigGroup.java new file mode 100644 index 00000000000000..6f6b621126a1c3 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigGroup.java @@ -0,0 +1,11 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import io.quarkus.annotation.processor.documentation.config.model.Extension; + +public final class DiscoveryConfigGroup extends DiscoveryRootElement { + + public DiscoveryConfigGroup(Extension extension, String binaryName, String qualifiedName, boolean configMapping) { + super(extension, binaryName, qualifiedName, configMapping); + } + +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigProperty.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigProperty.java new file mode 100644 index 00000000000000..cbff969922b77d --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigProperty.java @@ -0,0 +1,224 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import io.quarkus.annotation.processor.documentation.config.model.Deprecation; +import io.quarkus.annotation.processor.documentation.config.model.SourceType; +import io.quarkus.annotation.processor.documentation.config.util.TypeUtil; +import io.quarkus.annotation.processor.util.Strings; + +public class DiscoveryConfigProperty { + + private final String path; + private final String sourceClass; + private final String sourceName; + private final SourceType sourceType; + private final String defaultValue; + private final String defaultValueForDoc; + private final Deprecation deprecation; + private final String mapKey; + private final boolean unnamedMapKey; + private final ResolvedType type; + private final boolean converted; + private final boolean enforceHyphenateEnumValue; + private final boolean section; + private final boolean sectionGenerated; + + public DiscoveryConfigProperty(String path, String sourceClass, String sourceName, SourceType sourceType, + String defaultValue, + String defaultValueForDoc, Deprecation deprecation, String mapKey, boolean unnamedMapKey, + ResolvedType type, boolean converted, boolean enforceHyphenateEnumValue, + boolean section, boolean sectionGenerated) { + this.path = path; + this.sourceClass = sourceClass; + this.sourceName = sourceName; + this.sourceType = sourceType; + this.defaultValue = defaultValue; + this.defaultValueForDoc = defaultValueForDoc; + this.deprecation = deprecation; + this.mapKey = mapKey; + this.unnamedMapKey = unnamedMapKey; + this.type = type; + this.converted = converted; + this.enforceHyphenateEnumValue = enforceHyphenateEnumValue; + this.section = section; + this.sectionGenerated = sectionGenerated; + } + + public String getPath() { + return path; + } + + public String getSourceClass() { + return sourceClass; + } + + public String getSourceName() { + return sourceName; + } + + public SourceType getSourceType() { + return sourceType; + } + + public String getDefaultValue() { + return defaultValue; + } + + public String getDefaultValueForDoc() { + return defaultValueForDoc; + } + + public Deprecation getDeprecation() { + return deprecation; + } + + public boolean isDeprecated() { + return deprecation != null; + } + + public String getMapKey() { + return mapKey; + } + + public boolean isUnnamedMapKey() { + return unnamedMapKey; + } + + public ResolvedType getType() { + return type; + } + + public boolean isConverted() { + return converted; + } + + public boolean isEnforceHyphenateEnumValue() { + return enforceHyphenateEnumValue; + } + + public boolean isSection() { + return section; + } + + public boolean isSectionGenerated() { + return sectionGenerated; + } + + public String toString() { + return toString(""); + } + + public String toString(String prefix) { + StringBuilder sb = new StringBuilder(); + sb.append(prefix + "name = " + path + "\n"); + sb.append(prefix + "sourceClass = " + sourceClass + "\n"); + sb.append(prefix + "sourceName = " + sourceName + "\n"); + sb.append(prefix + "type = " + type + "\n"); + if (defaultValue != null) { + sb.append(prefix + "defaultValue = " + defaultValue + "\n"); + } + if (defaultValueForDoc != null) { + sb.append(prefix + "defaultValueForDoc = " + defaultValueForDoc + "\n"); + } + if (deprecation != null) { + sb.append(prefix + "deprecated = true\n"); + } + if (mapKey != null) { + sb.append(prefix + "mapKey = " + mapKey + "\n"); + } + if (unnamedMapKey) { + sb.append(prefix + "unnamedMapKey = true\n"); + } + if (converted) { + sb.append(prefix + "converted = true\n"); + } + + return sb.toString(); + } + + public static Builder builder(String sourceClass, String sourceName, SourceType sourceType, ResolvedType type) { + return new Builder(sourceClass, sourceName, sourceType, type); + } + + public static class Builder { + + private String name; + private final String sourceClass; + private final String sourceName; + private final SourceType sourceType; + private final ResolvedType type; + private String defaultValue; + private String defaultValueForDoc; + private Deprecation deprecation; + private String mapKey; + private boolean unnamedMapKey = false; + private boolean converted = false; + private boolean enforceHyphenateEnumValue = false; + private boolean section = false; + private boolean sectionGenerated = false; + + public Builder(String sourceClass, String sourceName, SourceType sourceType, ResolvedType type) { + this.sourceClass = sourceClass; + this.sourceName = sourceName; + this.sourceType = sourceType; + this.type = type; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public Builder defaultValueForDoc(String defaultValueForDoc) { + this.defaultValueForDoc = defaultValueForDoc; + return this; + } + + public Builder deprecated(String since, String replacement, String reason) { + this.deprecation = new Deprecation(since, replacement, reason); + return this; + } + + public Builder mapKey(String mapKey) { + this.mapKey = mapKey; + return this; + } + + public Builder unnamedMapKey() { + this.unnamedMapKey = true; + return this; + } + + public Builder converted() { + this.converted = true; + return this; + } + + public Builder enforceHyphenateEnumValues() { + this.enforceHyphenateEnumValue = true; + return this; + } + + public Builder section(boolean generated) { + this.section = true; + this.sectionGenerated = generated; + return this; + } + + public DiscoveryConfigProperty build() { + if (type.isPrimitive() && defaultValue == null) { + defaultValue = TypeUtil.getPrimitiveDefaultValue(type.qualifiedName()); + } + if (type.isDuration() && !Strings.isBlank(defaultValue)) { + defaultValue = TypeUtil.normalizeDurationValue(defaultValue); + } + + return new DiscoveryConfigProperty(name, sourceClass, sourceName, sourceType, defaultValue, defaultValueForDoc, + deprecation, mapKey, unnamedMapKey, type, converted, enforceHyphenateEnumValue, section, sectionGenerated); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigRoot.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigRoot.java new file mode 100644 index 00000000000000..86564da225709a --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigRoot.java @@ -0,0 +1,60 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import io.quarkus.annotation.processor.documentation.config.model.ConfigPhase; +import io.quarkus.annotation.processor.documentation.config.model.Extension; + +/** + * At this stage, each {@code @ConfigRoot} annotation leads to a separate DiscoveryConfigRoot. + * So you basically get one DiscoveryConfigRoot per phase. + * The config roots will get merged when we resolve the final model. + */ +public final class DiscoveryConfigRoot extends DiscoveryRootElement { + + private final String prefix; + private final String overriddenDocPrefix; + private final ConfigPhase phase; + private final String overriddenDocFileName; + + public DiscoveryConfigRoot(Extension extension, String prefix, String overriddenDocPrefix, + String binaryName, String qualifiedName, + ConfigPhase configPhase, String overriddenDocFileName, boolean configMapping) { + super(extension, binaryName, qualifiedName, configMapping); + + this.prefix = prefix; + this.overriddenDocPrefix = overriddenDocPrefix; + this.phase = configPhase; + this.overriddenDocFileName = overriddenDocFileName; + } + + public String getPrefix() { + return prefix; + } + + public String getOverriddenDocPrefix() { + return overriddenDocPrefix; + } + + public ConfigPhase getPhase() { + return phase; + } + + public String getOverriddenDocFileName() { + return overriddenDocFileName; + } + + public String toString() { + return toString(""); + } + + public String toString(String prefix) { + StringBuilder sb = new StringBuilder(); + sb.append(prefix + "prefix = " + this.prefix + "\n"); + sb.append(prefix + "config = " + this.phase + "\n"); + if (overriddenDocFileName != null) { + sb.append(prefix + "overriddenDocFileName = " + this.overriddenDocFileName + "\n"); + } + sb.append(super.toString(prefix)); + + return sb.toString(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryRootElement.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryRootElement.java new file mode 100644 index 00000000000000..8a0b39ccd483c5 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryRootElement.java @@ -0,0 +1,70 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.quarkus.annotation.processor.documentation.config.model.Extension; + +public sealed abstract class DiscoveryRootElement permits DiscoveryConfigRoot, DiscoveryConfigGroup { + + private final Extension extension; + private final String binaryName; + private final String qualifiedName; + private final Map properties = new LinkedHashMap<>(); + + // TODO #42114 remove once fixed + // this is an approximation, we can't fully detect that in the case of config groups + @Deprecated(forRemoval = true) + private final boolean configMapping; + + DiscoveryRootElement(Extension extension, String binaryName, String qualifiedName, boolean configMapping) { + this.extension = extension; + this.binaryName = binaryName; + this.qualifiedName = qualifiedName; + this.configMapping = configMapping; + } + + public Extension getExtension() { + return extension; + } + + public String getBinaryName() { + return binaryName; + } + + public String getQualifiedName() { + return qualifiedName; + } + + public void addProperty(DiscoveryConfigProperty discoveryConfigProperty) { + properties.put(discoveryConfigProperty.getSourceName(), discoveryConfigProperty); + } + + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + @Deprecated(forRemoval = true) + public boolean isConfigMapping() { + return configMapping; + } + + public String toString() { + return toString(""); + } + + public String toString(String prefix) { + StringBuilder sb = new StringBuilder(); + sb.append(prefix + "binaryName = " + this.binaryName); + + if (!properties.isEmpty()) { + sb.append("\n\n" + prefix + "--- Properties ---\n\n"); + for (DiscoveryConfigProperty property : properties.values()) { + sb.append(property.toString(prefix) + prefix + "--\n"); + } + } + + return sb.toString(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/EnumDefinition.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/EnumDefinition.java new file mode 100644 index 00000000000000..4f500cc734c412 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/EnumDefinition.java @@ -0,0 +1,19 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import java.util.Map; + +/** + * This is an uncontextual enum definition obtained from the enum type. + *

+ * At resolution, the enum will get contextualized to how it's consumed in the config property (and for instance, might be + * hyphenated if needed). + */ +public record EnumDefinition(String qualifiedName, Map constants) { + + public record EnumConstant(String explicitValue) { + + public boolean hasExplicitValue() { + return explicitValue != null; + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java new file mode 100644 index 00000000000000..3e6860a917ebc9 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java @@ -0,0 +1,14 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public record ParsedJavadoc(String description, JavadocFormat format, String since, String deprecated) { + + public static ParsedJavadoc empty() { + return new ParsedJavadoc(null, null, null, null); + } + + public boolean isEmpty() { + return description == null || description.isBlank(); + } +} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java new file mode 100644 index 00000000000000..6d1cbbabc6cc13 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java @@ -0,0 +1,10 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public record ParsedJavadocSection(String title, String details, JavadocFormat format, String deprecated) { + + public static ParsedJavadocSection empty() { + return new ParsedJavadocSection(null, null, null, null); + } +} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ResolvedType.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ResolvedType.java new file mode 100644 index 00000000000000..c2a3b9bf223d01 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ResolvedType.java @@ -0,0 +1,78 @@ +package io.quarkus.annotation.processor.documentation.config.discovery; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +public record ResolvedType( + TypeMirror wrapperType, + TypeMirror unwrappedType, + String binaryName, + String qualifiedName, + String simplifiedName, + boolean isPrimitive, + boolean isMap, + boolean isList, + boolean isOptional, + boolean isDeclared, + boolean isInterface, + boolean isClass, + boolean isEnum, + boolean isDuration, + boolean isConfigGroup) { + + public TypeElement unwrappedTypeElement() { + if (!isDeclared) { + throw new IllegalStateException("Unable to get element as unwrappedType is not a DeclaredType: " + unwrappedType); + } + + return (TypeElement) ((DeclaredType) unwrappedType).asElement(); + } + + @Override + public final String toString() { + return unwrappedType.toString(); + } + + public static ResolvedType ofPrimitive(TypeMirror unwrappedType, String typeName) { + return new ResolvedType(unwrappedType, unwrappedType, typeName, typeName, typeName, true, false, false, + false, false, false, false, false, false, false); + } + + public static ResolvedType ofDeclaredType(TypeMirror type, String binaryName, + String qualifiedName, String simpleName, + boolean isInterface, boolean isClass, boolean isEnum, boolean isDuration, boolean isConfigGroup) { + return new ResolvedType(type, type, binaryName, qualifiedName, simpleName, false, false, false, false, true, + isInterface, isClass, isEnum, isDuration, isConfigGroup); + } + + public static ResolvedType makeList(TypeMirror type, ResolvedType unwrappedResolvedType) { + return new ResolvedType(type, unwrappedResolvedType.unwrappedType, + unwrappedResolvedType.binaryName, unwrappedResolvedType.qualifiedName, unwrappedResolvedType.simplifiedName, + unwrappedResolvedType.isPrimitive, + unwrappedResolvedType.isMap, true, + unwrappedResolvedType.isOptional, + unwrappedResolvedType.isDeclared, unwrappedResolvedType.isInterface, unwrappedResolvedType.isClass, + unwrappedResolvedType.isEnum, unwrappedResolvedType.isDuration, unwrappedResolvedType.isConfigGroup); + } + + public static ResolvedType makeOptional(ResolvedType unwrappedResolvedType) { + return new ResolvedType(unwrappedResolvedType.wrapperType, unwrappedResolvedType.unwrappedType, + unwrappedResolvedType.binaryName, unwrappedResolvedType.qualifiedName, unwrappedResolvedType.simplifiedName, + unwrappedResolvedType.isPrimitive, + unwrappedResolvedType.isMap, unwrappedResolvedType.isList, + true, + unwrappedResolvedType.isDeclared, unwrappedResolvedType.isInterface, unwrappedResolvedType.isClass, + unwrappedResolvedType.isEnum, unwrappedResolvedType.isDuration, unwrappedResolvedType.isConfigGroup); + } + + public static ResolvedType makeMap(TypeMirror type, ResolvedType unwrappedResolvedType) { + return new ResolvedType(type, unwrappedResolvedType.unwrappedType, + unwrappedResolvedType.binaryName, unwrappedResolvedType.qualifiedName, unwrappedResolvedType.simplifiedName, + unwrappedResolvedType.isPrimitive, + true, false, + unwrappedResolvedType.isOptional, + unwrappedResolvedType.isDeclared, unwrappedResolvedType.isInterface, unwrappedResolvedType.isClass, + unwrappedResolvedType.isEnum, unwrappedResolvedType.isDuration, unwrappedResolvedType.isConfigGroup); + } +} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java new file mode 100644 index 00000000000000..5007f9ce71bc01 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java @@ -0,0 +1,443 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import java.util.regex.Pattern; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.description.JavadocDescriptionElement; +import com.github.javaparser.javadoc.description.JavadocInlineTag; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; +import io.quarkus.annotation.processor.documentation.config.util.ConfigNamingUtil; + +public final class JavadocToAsciidocTransformer { + + private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); + private static final Pattern STARTING_SPACE = Pattern.compile("^ +"); + + private static final String BACKTICK = "`"; + private static final String HASH = "#"; + private static final String STAR = "*"; + private static final String S_NODE = "s"; + private static final String UNDERSCORE = "_"; + private static final String NEW_LINE = "\n"; + private static final String LINK_NODE = "a"; + private static final String BOLD_NODE = "b"; + private static final String STRONG_NODE = "strong"; + private static final String BIG_NODE = "big"; + private static final String CODE_NODE = "code"; + private static final String DEL_NODE = "del"; + private static final String ITALICS_NODE = "i"; + private static final String EMPHASIS_NODE = "em"; + private static final String TEXT_NODE = "#text"; + private static final String UNDERLINE_NODE = "u"; + private static final String NEW_LINE_NODE = "br"; + private static final String PARAGRAPH_NODE = "p"; + private static final String SMALL_NODE = "small"; + private static final String LIST_ITEM_NODE = "li"; + private static final String HREF_ATTRIBUTE = "href"; + private static final String STRIKE_NODE = "strike"; + private static final String SUB_SCRIPT_NODE = "sub"; + private static final String ORDERED_LIST_NODE = "ol"; + private static final String SUPER_SCRIPT_NODE = "sup"; + private static final String UN_ORDERED_LIST_NODE = "ul"; + private static final String PREFORMATED_NODE = "pre"; + private static final String BLOCKQUOTE_NODE = "blockquote"; + + private static final String BIG_ASCIDOC_STYLE = "[.big]"; + private static final String LINK_ATTRIBUTE_FORMAT = "[%s]"; + private static final String SUB_SCRIPT_ASCIDOC_STYLE = "~"; + private static final String SUPER_SCRIPT_ASCIDOC_STYLE = "^"; + private static final String SMALL_ASCIDOC_STYLE = "[.small]"; + private static final String ORDERED_LIST_ITEM_ASCIDOC_STYLE = " . "; + private static final String UNORDERED_LIST_ITEM_ASCIDOC_STYLE = " - "; + private static final String UNDERLINE_ASCIDOC_STYLE = "[.underline]"; + private static final String LINE_THROUGH_ASCIDOC_STYLE = "[.line-through]"; + private static final String HARD_LINE_BREAK_ASCIDOC_STYLE = " +\n"; + private static final String CODE_BLOCK_ASCIDOC_STYLE = "```"; + private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE = "[quote]\n____"; + private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END = "____"; + + private JavadocToAsciidocTransformer() { + } + + public static String toAsciidoc(String javadoc, JavadocFormat format) { + return toAsciidoc(javadoc, format, false); + } + + public static String toAsciidoc(String javadoc, JavadocFormat format, boolean inlineMacroMode) { + if (javadoc == null || javadoc.isBlank()) { + return null; + } + + if (format == JavadocFormat.ASCIIDOC) { + return javadoc; + } else if (format == JavadocFormat.MARKDOWN) { + throw new IllegalArgumentException("Conversion from Markdown to Asciidoc is not supported"); + } + + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + Javadoc parsedJavadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(javadoc).replaceAll("* ")); + + StringBuilder sb = new StringBuilder(); + + for (JavadocDescriptionElement javadocDescriptionElement : parsedJavadoc.getDescription().getElements()) { + if (javadocDescriptionElement instanceof JavadocInlineTag) { + JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; + String content = inlineTag.getContent().trim(); + switch (inlineTag.getType()) { + case CODE: + case VALUE: + case LITERAL: + case SYSTEM_PROPERTY: + sb.append('`'); + appendEscapedAsciiDoc(sb, content, inlineMacroMode); + sb.append('`'); + break; + case LINK: + case LINKPLAIN: + if (content.startsWith(HASH)) { + content = ConfigNamingUtil.hyphenate(content.substring(1)); + } + sb.append('`'); + appendEscapedAsciiDoc(sb, content, inlineMacroMode); + sb.append('`'); + break; + default: + sb.append(content); + break; + } + } else { + appendHtml(sb, Jsoup.parseBodyFragment(javadocDescriptionElement.toText()), inlineMacroMode); + } + } + + String asciidoc = trim(sb); + + return asciidoc.isBlank() ? null : asciidoc; + } + + private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroMode) { + for (Node childNode : node.childNodes()) { + switch (childNode.nodeName()) { + case PARAGRAPH_NODE: + newLine(sb); + newLine(sb); + appendHtml(sb, childNode, inlineMacroMode); + break; + case PREFORMATED_NODE: + newLine(sb); + newLine(sb); + sb.append(CODE_BLOCK_ASCIDOC_STYLE); + newLine(sb); + for (Node grandChildNode : childNode.childNodes()) { + unescapeHtmlEntities(sb, grandChildNode.toString()); + } + newLineIfNeeded(sb); + sb.append(CODE_BLOCK_ASCIDOC_STYLE); + newLine(sb); + newLine(sb); + break; + case BLOCKQUOTE_NODE: + newLine(sb); + newLine(sb); + sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE); + newLine(sb); + appendHtml(sb, childNode, inlineMacroMode); + newLineIfNeeded(sb); + sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END); + newLine(sb); + newLine(sb); + break; + case ORDERED_LIST_NODE: + case UN_ORDERED_LIST_NODE: + newLine(sb); + appendHtml(sb, childNode, inlineMacroMode); + break; + case LIST_ITEM_NODE: + final String marker = childNode.parent().nodeName().equals(ORDERED_LIST_NODE) + ? ORDERED_LIST_ITEM_ASCIDOC_STYLE + : UNORDERED_LIST_ITEM_ASCIDOC_STYLE; + newLine(sb); + sb.append(marker); + appendHtml(sb, childNode, inlineMacroMode); + break; + case LINK_NODE: + final String link = childNode.attr(HREF_ATTRIBUTE); + sb.append("link:"); + sb.append(link); + final StringBuilder caption = new StringBuilder(); + appendHtml(caption, childNode, inlineMacroMode); + sb.append(String.format(LINK_ATTRIBUTE_FORMAT, trim(caption))); + break; + case CODE_NODE: + sb.append(BACKTICK); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(BACKTICK); + break; + case BOLD_NODE: + case STRONG_NODE: + sb.append(STAR); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(STAR); + break; + case EMPHASIS_NODE: + case ITALICS_NODE: + sb.append(UNDERSCORE); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(UNDERSCORE); + break; + case UNDERLINE_NODE: + sb.append(UNDERLINE_ASCIDOC_STYLE); + sb.append(HASH); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(HASH); + break; + case SMALL_NODE: + sb.append(SMALL_ASCIDOC_STYLE); + sb.append(HASH); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(HASH); + break; + case BIG_NODE: + sb.append(BIG_ASCIDOC_STYLE); + sb.append(HASH); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(HASH); + break; + case SUB_SCRIPT_NODE: + sb.append(SUB_SCRIPT_ASCIDOC_STYLE); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(SUB_SCRIPT_ASCIDOC_STYLE); + break; + case SUPER_SCRIPT_NODE: + sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); + break; + case DEL_NODE: + case S_NODE: + case STRIKE_NODE: + sb.append(LINE_THROUGH_ASCIDOC_STYLE); + sb.append(HASH); + appendHtml(sb, childNode, inlineMacroMode); + sb.append(HASH); + break; + case NEW_LINE_NODE: + sb.append(HARD_LINE_BREAK_ASCIDOC_STYLE); + break; + case TEXT_NODE: + String text = ((TextNode) childNode).text(); + + if (text.isEmpty()) { + break; + } + + // Indenting the first line of a paragraph by one or more spaces makes the block literal + // Please see https://docs.asciidoctor.org/asciidoc/latest/verbatim/literal-blocks/ for more info + // This prevents literal blocks f.e. after
+ final var startingSpaceMatcher = STARTING_SPACE.matcher(text); + if (sb.length() > 0 && '\n' == sb.charAt(sb.length() - 1) && startingSpaceMatcher.find()) { + text = startingSpaceMatcher.replaceFirst(""); + } + + appendEscapedAsciiDoc(sb, text, inlineMacroMode); + break; + default: + appendHtml(sb, childNode, inlineMacroMode); + break; + } + } + } + + /** + * Trim the content of the given {@link StringBuilder} holding also AsciiDoc had line break {@code " +\n"} + * for whitespace in addition to characters <= {@code ' '}. + * + * @param sb the {@link StringBuilder} to trim + * @return the trimmed content of the given {@link StringBuilder} + */ + static String trim(StringBuilder sb) { + int length = sb.length(); + int offset = 0; + while (offset < length) { + final char ch = sb.charAt(offset); + if (ch == ' ' + && offset + 2 < length + && sb.charAt(offset + 1) == '+' + && sb.charAt(offset + 2) == '\n') { + /* Space followed by + and newline is AsciiDoc hard break that we consider whitespace */ + offset += 3; + continue; + } else if (ch > ' ') { + /* Non-whitespace as defined by String.trim() */ + break; + } + offset++; + } + if (offset > 0) { + sb.delete(0, offset); + } + if (sb.length() > 0) { + offset = sb.length() - 1; + while (offset >= 0) { + final char ch = sb.charAt(offset); + if (ch == '\n' + && offset - 2 >= 0 + && sb.charAt(offset - 1) == '+' + && sb.charAt(offset - 2) == ' ') { + /* Space followed by + is AsciiDoc hard break that we consider whitespace */ + offset -= 3; + continue; + } else if (ch > ' ') { + /* Non-whitespace as defined by String.trim() */ + break; + } + offset--; + } + if (offset < sb.length() - 1) { + sb.setLength(offset + 1); + } + } + return sb.toString(); + } + + private static StringBuilder newLineIfNeeded(StringBuilder sb) { + trimText(sb, " \t\r\n"); + return sb.append(NEW_LINE); + } + + private static StringBuilder newLine(StringBuilder sb) { + /* Trim trailing spaces and tabs at the end of line */ + trimText(sb, " \t"); + return sb.append(NEW_LINE); + } + + private static StringBuilder trimText(StringBuilder sb, String charsToTrim) { + while (sb.length() > 0 && charsToTrim.indexOf(sb.charAt(sb.length() - 1)) >= 0) { + sb.setLength(sb.length() - 1); + } + return sb; + } + + private static StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { + int i = 0; + /* trim leading whitespace */ + LOOP: while (i < text.length()) { + switch (text.charAt(i++)) { + case ' ': + case '\t': + case '\r': + case '\n': + break; + default: + i--; + break LOOP; + } + } + for (; i < text.length(); i++) { + final char ch = text.charAt(i); + switch (ch) { + case '&': + int start = ++i; + while (i < text.length() && text.charAt(i) != ';') { + i++; + } + if (i > start) { + final String abbrev = text.substring(start, i); + switch (abbrev) { + case "lt": + sb.append('<'); + break; + case "gt": + sb.append('>'); + break; + case "nbsp": + sb.append("{nbsp}"); + break; + case "amp": + sb.append('&'); + break; + default: + try { + int code = Integer.parseInt(abbrev); + sb.append((char) code); + } catch (NumberFormatException e) { + throw new RuntimeException( + "Could not parse HTML entity &" + abbrev + "; in\n\n" + text + "\n\n"); + } + break; + } + } + break; + case '\r': + if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { + /* Ignore \r followed by \n */ + } else { + /* A Mac single \r: replace by \n */ + sb.append('\n'); + } + break; + default: + sb.append(ch); + + } + } + return sb; + } + + private static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text, boolean inlineMacroMode) { + boolean escaping = false; + for (int i = 0; i < text.length(); i++) { + final char ch = text.charAt(i); + switch (ch) { + case ']': + // don't escape closing square bracket in the attribute list of an inline element with passThrough + // https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#substitutions + if (inlineMacroMode) { + if (escaping) { + sb.append("++"); + escaping = false; + } + sb.append("]"); + break; + } + case '#': + case '*': + case '\\': + case '{': + case '}': + case '[': + case '|': + if (!escaping) { + sb.append("++"); + escaping = true; + } + sb.append(ch); + break; + case '+': + if (escaping) { + sb.append("++"); + escaping = false; + } + sb.append("{plus}"); + break; + default: + if (escaping) { + sb.append("++"); + escaping = false; + } + sb.append(ch); + } + } + if (escaping) { + sb.append("++"); + } + return sb; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java new file mode 100644 index 00000000000000..fd5796ed7b8b0e --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java @@ -0,0 +1,87 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import java.util.regex.Pattern; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.description.JavadocDescription; +import com.github.javaparser.javadoc.description.JavadocDescriptionElement; +import com.github.javaparser.javadoc.description.JavadocInlineTag; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public class JavadocToMarkdownTransformer { + + private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); + + public static String toMarkdown(String javadoc, JavadocFormat format) { + if (javadoc == null || javadoc.isBlank()) { + return null; + } + + if (format == JavadocFormat.MARKDOWN) { + return javadoc; + } else if (format == JavadocFormat.JAVADOC) { + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + Javadoc parsedJavadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(javadoc).replaceAll("* ")); + + // HTML is valid Javadoc but we need to drop the Javadoc tags e.g. {@link ...} + return simplifyJavadoc(parsedJavadoc.getDescription()); + } + + // it's Asciidoc, the fun begins... + return ""; + } + + /** + * This is not definitely not perfect but it can be used to filter the Javadoc included in Markdown. + *

+ * We will need to discuss further how to handle passing the Javadoc to the IDE. + * In Quarkus, we have Asciidoc, standard Javadoc and soon we might have Markdown javadoc. + */ + private static String simplifyJavadoc(JavadocDescription javadocDescription) { + StringBuilder sb = new StringBuilder(); + + for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { + if (javadocDescriptionElement instanceof JavadocInlineTag) { + JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; + String content = inlineTag.getContent().trim(); + switch (inlineTag.getType()) { + case CODE: + case VALUE: + case LITERAL: + case SYSTEM_PROPERTY: + case LINK: + case LINKPLAIN: + sb.append(""); + sb.append(escapeHtml(content)); + sb.append(""); + break; + default: + sb.append(content); + break; + } + } else { + sb.append(javadocDescriptionElement.toText()); + } + } + + return sb.toString().trim(); + } + + private static String escapeHtml(String s) { + StringBuilder out = new StringBuilder(Math.max(16, s.length())); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') { + out.append("&#"); + out.append((int) c); + out.append(';'); + } else { + out.append(c); + } + } + return out.toString(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java new file mode 100644 index 00000000000000..59da647afdfa3e --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java @@ -0,0 +1,20 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public final class JavadocTransformer { + + private JavadocTransformer() { + } + + public static String transform(String javadoc, JavadocFormat fromFormat, JavadocFormat toFormat) { + switch (toFormat) { + case ASCIIDOC: + return JavadocToAsciidocTransformer.toAsciidoc(javadoc, fromFormat); + case MARKDOWN: + return JavadocToMarkdownTransformer.toMarkdown(javadoc, fromFormat); + default: + throw new IllegalArgumentException("Converting to " + toFormat + " is not supported"); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocMerger.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocMerger.java new file mode 100644 index 00000000000000..d74042b0508f4b --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocMerger.java @@ -0,0 +1,46 @@ +package io.quarkus.annotation.processor.documentation.config.merger; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.annotation.processor.Outputs; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; + +public final class JavadocMerger { + + private JavadocMerger() { + } + + public static JavadocRepository mergeJavadocElements(List buildOutputDirectories) { + Map javadocElementsMap = new HashMap<>(); + + for (Path buildOutputDirectory : buildOutputDirectories) { + Path javadocPath = buildOutputDirectory.resolve(Outputs.QUARKUS_CONFIG_DOC_JAVADOC); + if (!Files.isReadable(javadocPath)) { + continue; + } + + try (InputStream javadocIs = Files.newInputStream(javadocPath)) { + JavadocElements javadocElements = JacksonMappers.yamlObjectReader().readValue(javadocIs, + JavadocElements.class); + + if (javadocElements.elements() == null || javadocElements.elements().isEmpty()) { + continue; + } + + javadocElementsMap.putAll(javadocElements.elements()); + } catch (IOException e) { + throw new IllegalStateException("Unable to parse: " + javadocPath, e); + } + } + + return new JavadocRepository(javadocElementsMap); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocRepository.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocRepository.java new file mode 100644 index 00000000000000..5509b0c820c49d --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocRepository.java @@ -0,0 +1,24 @@ +package io.quarkus.annotation.processor.documentation.config.merger; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.Markers; + +public final class JavadocRepository { + + private final Map javadocElementsMap; + + JavadocRepository(Map javadocElementsMap) { + this.javadocElementsMap = javadocElementsMap; + } + + public Optional getElement(String className, String elementName) { + return Optional.ofNullable(javadocElementsMap.get(className + Markers.DOT + elementName)); + } + + public Optional getElement(String className) { + return Optional.ofNullable(javadocElementsMap.get(className)); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/MergedModel.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/MergedModel.java new file mode 100644 index 00000000000000..a2fbac9fa7f264 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/MergedModel.java @@ -0,0 +1,72 @@ +package io.quarkus.annotation.processor.documentation.config.merger; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; + +/** + * The merged model we obtain after merging all the ResolvedModels from the current project. + */ +public class MergedModel { + + private final Map> configRoots; + + private final Map configRootsInSpecificFile; + + private final Map> generatedConfigSections; + + MergedModel(Map> configRoots, + Map configRootsInSpecificFile, + Map> configSections) { + this.configRoots = Collections.unmodifiableMap(configRoots); + this.configRootsInSpecificFile = Collections.unmodifiableMap(configRootsInSpecificFile); + this.generatedConfigSections = Collections.unmodifiableMap(configSections); + } + + public Map> getConfigRoots() { + return configRoots; + } + + public Map getConfigRootsInSpecificFile() { + return configRootsInSpecificFile; + } + + public Map> getGeneratedConfigSections() { + return generatedConfigSections; + } + + public boolean isEmpty() { + return configRoots.isEmpty(); + } + + public record ConfigRootKey(String topLevelPrefix, String description) implements Comparable { + + @Override + public final String toString() { + return topLevelPrefix; + } + + @Override + public int compareTo(ConfigRootKey other) { + int compareTopLevelPrefix = this.topLevelPrefix.compareToIgnoreCase(other.topLevelPrefix); + if (compareTopLevelPrefix != 0) { + return compareTopLevelPrefix; + } + if (this.description == null && other.description == null) { + return 0; + } + if (this.description == null) { + return -1; + } + if (other.description == null) { + return 1; + } + + return this.description.compareToIgnoreCase(other.description); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/ModelMerger.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/ModelMerger.java new file mode 100644 index 00000000000000..4deff0ca7c3bea --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/ModelMerger.java @@ -0,0 +1,200 @@ +package io.quarkus.annotation.processor.documentation.config.merger; + +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import io.quarkus.annotation.processor.Outputs; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; +import io.quarkus.annotation.processor.documentation.config.model.AbstractConfigItem; +import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; +import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.annotation.processor.documentation.config.model.Extension.NameSource; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.model.ResolvedModel; +import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; + +public final class ModelMerger { + + private ModelMerger() { + } + + /** + * Merge all the resolved models obtained from a list of build output directories (e.g. in the case of Maven, the list of + * target/ directories found in the parent directory scanned). + */ + public static MergedModel mergeModel(List buildOutputDirectories) { + return mergeModel(null, buildOutputDirectories); + } + + /** + * Merge all the resolved models obtained from a list of build output directories (e.g. in the case of Maven, the list of + * target/ directories found in the parent directory scanned). + */ + public static MergedModel mergeModel(JavadocRepository javadocRepository, List buildOutputDirectories) { + // keyed on extension and then top level prefix + Map> configRoots = new HashMap<>(); + // keyed on file name + Map configRootsInSpecificFile = new TreeMap<>(); + // keyed on extension + Map> generatedConfigSections = new TreeMap<>(); + + for (Path buildOutputDirectory : buildOutputDirectories) { + Path resolvedModelPath = buildOutputDirectory.resolve(Outputs.QUARKUS_CONFIG_DOC_MODEL); + if (!Files.isReadable(resolvedModelPath)) { + continue; + } + + try (InputStream resolvedModelIs = Files.newInputStream(resolvedModelPath)) { + ResolvedModel resolvedModel = JacksonMappers.yamlObjectReader().readValue(resolvedModelIs, + ResolvedModel.class); + + if (resolvedModel.getConfigRoots() == null || resolvedModel.getConfigRoots().isEmpty()) { + continue; + } + + for (ConfigRoot configRoot : resolvedModel.getConfigRoots()) { + if (configRoot.getOverriddenDocFileName() != null) { + ConfigRoot existingConfigRootInSpecificFile = configRootsInSpecificFile + .get(configRoot.getOverriddenDocFileName()); + + if (existingConfigRootInSpecificFile == null) { + configRootsInSpecificFile.put(configRoot.getOverriddenDocFileName(), configRoot); + } else { + if (!existingConfigRootInSpecificFile.getExtension().equals(configRoot.getExtension()) + || !existingConfigRootInSpecificFile.getPrefix().equals(configRoot.getPrefix())) { + throw new IllegalStateException( + "Two config roots with different extensions or prefixes cannot be merged in the same specific config file: " + + configRoot.getOverriddenDocFileName()); + } + + existingConfigRootInSpecificFile.merge(configRoot); + } + + continue; + } + + Map extensionConfigRoots = configRoots.computeIfAbsent(configRoot.getExtension(), + e -> new TreeMap<>()); + + ConfigRootKey configRootKey = getConfigRootKey(javadocRepository, configRoot); + ConfigRoot existingConfigRoot = extensionConfigRoots.get(configRootKey); + + if (existingConfigRoot == null) { + extensionConfigRoots.put(configRootKey, configRoot); + } else { + existingConfigRoot.merge(configRoot); + } + } + } catch (IOException e) { + throw new IllegalStateException("Unable to parse: " + resolvedModelPath, e); + } + } + + configRoots = retainBestExtensionKey(configRoots); + + for (Entry> extensionConfigRootsEntry : configRoots.entrySet()) { + List extensionGeneratedConfigSections = generatedConfigSections + .computeIfAbsent(extensionConfigRootsEntry.getKey(), e -> new ArrayList<>()); + + for (ConfigRoot configRoot : extensionConfigRootsEntry.getValue().values()) { + collectGeneratedConfigSections(extensionGeneratedConfigSections, configRoot); + } + } + + return new MergedModel(configRoots, configRootsInSpecificFile, generatedConfigSections); + } + + private static Map> retainBestExtensionKey( + Map> configRoots) { + return configRoots.entrySet().stream().collect(Collectors.toMap(e -> { + Extension extension = e.getKey(); + + for (ConfigRoot configRoot : e.getValue().values()) { + if (configRoot.getExtension().nameSource().isBetterThan(extension.nameSource())) { + extension = configRoot.getExtension(); + } + if (NameSource.EXTENSION_METADATA.equals(extension.nameSource())) { + // we won't find any better + break; + } + } + + return extension; + }, e -> e.getValue(), (k1, k2) -> k1, TreeMap::new)); + } + + private static void collectGeneratedConfigSections(List extensionGeneratedConfigSections, + ConfigItemCollection configItemCollection) { + for (AbstractConfigItem configItem : configItemCollection.getItems()) { + if (!configItem.isSection()) { + continue; + } + + ConfigSection configSection = (ConfigSection) configItem; + if (configSection.isGenerated()) { + extensionGeneratedConfigSections.add(configSection); + } + + collectGeneratedConfigSections(extensionGeneratedConfigSections, configSection); + } + } + + private static ConfigRootKey getConfigRootKey(JavadocRepository javadocRepository, ConfigRoot configRoot) { + return new ConfigRootKey(configRoot.getTopLevelPrefix(), getConfigRootDescription(javadocRepository, configRoot)); + } + + // here we only return a description if all the qualified names of the config root have a similar description + private static String getConfigRootDescription(JavadocRepository javadocRepository, ConfigRoot configRoot) { + if (!configRoot.getExtension().splitOnConfigRootDescription()) { + return null; + } + if (javadocRepository == null) { + return null; + } + + String description = null; + + for (String qualifiedName : configRoot.getQualifiedNames()) { + Optional javadocElement = javadocRepository.getElement(qualifiedName); + + if (javadocElement.isEmpty()) { + return null; + } + + if (description == null) { + description = trimFinalDot(javadocElement.get().description()); + } else if (!description.equals(trimFinalDot(javadocElement.get().description()))) { + return null; + } + } + + return description; + } + + private static String trimFinalDot(String javadoc) { + if (javadoc == null || javadoc.isBlank()) { + return null; + } + + javadoc = javadoc.trim(); + int dotIndex = javadoc.indexOf("."); + + if (dotIndex == -1) { + return javadoc; + } + + return javadoc.substring(0, dotIndex); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/AbstractConfigItem.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/AbstractConfigItem.java new file mode 100644 index 00000000000000..643ed96d286208 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/AbstractConfigItem.java @@ -0,0 +1,79 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") +public sealed abstract class AbstractConfigItem implements Comparable + permits ConfigProperty, ConfigSection { + + protected final String sourceClass; + protected final String sourceName; + protected final SourceType sourceType; + protected final Path path; + + protected final String type; + + protected Deprecation deprecation; + + public AbstractConfigItem(String sourceClass, String sourceName, SourceType sourceType, Path path, String type, + Deprecation deprecation) { + this.sourceClass = sourceClass; + this.sourceName = sourceName; + this.sourceType = sourceType; + this.path = path; + this.type = type; + this.deprecation = deprecation; + } + + public String getSourceClass() { + return sourceClass; + } + + public String getSourceName() { + return sourceName; + } + + public SourceType getSourceType() { + return sourceType; + } + + public Path getPath() { + return path; + } + + @Deprecated + @JsonIgnore + public String getPath$$bridge() { + return path.property(); + } + + public String getType() { + return type; + } + + @JsonIgnore + public boolean isDeprecated() { + return deprecation != null; + } + + public Deprecation getDeprecation() { + return deprecation; + } + + @JsonIgnore + public abstract boolean isSection(); + + @JsonIgnore + public abstract boolean hasDurationType(); + + @JsonIgnore + public abstract boolean hasMemorySizeType(); + + protected abstract void walk(ConfigItemVisitor visitor); + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") + public interface Path { + String property(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java new file mode 100644 index 00000000000000..77cd8dd3f1e53f --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java @@ -0,0 +1,39 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public interface ConfigItemCollection { + + List getItems(); + + @JsonIgnore + default List getNonDeprecatedItems() { + return getItems().stream() + .filter(i -> (i instanceof ConfigSection) + ? !i.isDeprecated() && ((ConfigSection) i).getNonDeprecatedItems().size() > 0 + : !i.isDeprecated()) + .toList(); + } + + @JsonIgnore + default List getNonDeprecatedProperties() { + return getItems().stream() + .filter(i -> i instanceof ConfigProperty && !i.isDeprecated()) + .toList(); + } + + @JsonIgnore + default List getNonDeprecatedSections() { + return getItems().stream() + .filter(i -> i instanceof ConfigSection && !i.isDeprecated()) + .toList(); + } + + void addItem(AbstractConfigItem item); + + boolean hasDurationType(); + + boolean hasMemorySizeType(); +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemVisitor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemVisitor.java new file mode 100644 index 00000000000000..1e62b2a8ce22c3 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemVisitor.java @@ -0,0 +1,6 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +public interface ConfigItemVisitor { + + public void visit(AbstractConfigItem configItem); +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigPhase.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigPhase.java new file mode 100644 index 00000000000000..e0aa79c43c1906 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigPhase.java @@ -0,0 +1,67 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.util.Comparator; + +public enum ConfigPhase implements Comparable { + + RUN_TIME("RunTime", false), + BUILD_TIME("BuildTime", true), + BUILD_AND_RUN_TIME_FIXED("BuildTime", true); + + static final Comparator COMPARATOR = new Comparator() { + /** + * Order built time phase first + * Then build time run time fixed phase + * Then runtime one + */ + @Override + public int compare(ConfigPhase firstPhase, ConfigPhase secondPhase) { + switch (firstPhase) { + case BUILD_TIME: { + switch (secondPhase) { + case BUILD_TIME: + return 0; + default: + return -1; + } + } + case BUILD_AND_RUN_TIME_FIXED: { + switch (secondPhase) { + case BUILD_TIME: + return 1; + case BUILD_AND_RUN_TIME_FIXED: + return 0; + default: + return -1; + } + } + case RUN_TIME: { + switch (secondPhase) { + case RUN_TIME: + return 0; + default: + return 1; + } + } + default: + return 0; + } + } + }; + + private final String configSuffix; + private final boolean fixedAtBuildTime; + + ConfigPhase(String configSuffix, boolean fixedAtBuildTime) { + this.configSuffix = configSuffix; + this.fixedAtBuildTime = fixedAtBuildTime; + } + + public String getConfigSuffix() { + return configSuffix; + } + + public boolean isFixedAtBuildTime() { + return fixedAtBuildTime; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigProperty.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigProperty.java new file mode 100644 index 00000000000000..1be3576d4e27dd --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigProperty.java @@ -0,0 +1,168 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.annotation.processor.documentation.config.util.Types; + +public final class ConfigProperty extends AbstractConfigItem { + + private final ConfigPhase phase; + private final List additionalPaths; + + private final String typeDescription; + private final boolean map; + private final boolean list; + private final boolean optional; + private final String mapKey; + private final boolean unnamedMapKey; + private final boolean withinMap; + private final boolean converted; + private final boolean isEnum; + private final EnumAcceptedValues enumAcceptedValues; + + private final String defaultValue; + + private final String javadocSiteLink; + + public ConfigProperty(ConfigPhase phase, String sourceClass, String sourceName, SourceType sourceType, PropertyPath path, + List additionalPaths, String type, String typeDescription, boolean map, boolean list, + boolean optional, + String mapKey, boolean unnamedMapKey, boolean withinMap, boolean converted, @JsonProperty("enum") boolean isEnum, + EnumAcceptedValues enumAcceptedValues, + String defaultValue, String javadocSiteLink, + Deprecation deprecation) { + super(sourceClass, sourceName, sourceType, path, type, deprecation); + this.phase = phase; + this.additionalPaths = additionalPaths != null ? Collections.unmodifiableList(additionalPaths) : List.of(); + this.typeDescription = typeDescription; + this.map = map; + this.list = list; + this.optional = optional; + this.mapKey = mapKey; + this.unnamedMapKey = unnamedMapKey; + this.withinMap = withinMap; + this.converted = converted; + this.isEnum = isEnum; + this.enumAcceptedValues = enumAcceptedValues; + this.defaultValue = defaultValue; + this.javadocSiteLink = javadocSiteLink; + } + + public ConfigPhase getPhase() { + return phase; + } + + public PropertyPath getPath() { + return (PropertyPath) super.getPath(); + } + + public List getAdditionalPaths() { + return additionalPaths; + } + + @Deprecated + @JsonIgnore + public String getEnvironmentVariable() { + return getPath().environmentVariable(); + } + + public String getTypeDescription() { + return typeDescription; + } + + public boolean isMap() { + return map; + } + + public boolean isList() { + return list; + } + + public boolean isOptional() { + return optional; + } + + public String getMapKey() { + return mapKey; + } + + public boolean isUnnamedMapKey() { + return unnamedMapKey; + } + + public boolean isWithinMap() { + return withinMap; + } + + public boolean isConverted() { + return converted; + } + + public boolean isEnum() { + return isEnum; + } + + public EnumAcceptedValues getEnumAcceptedValues() { + return enumAcceptedValues; + } + + public String getDefaultValue() { + return defaultValue; + } + + public String getJavadocSiteLink() { + return javadocSiteLink; + } + + public boolean isSection() { + return false; + } + + @Override + public int compareTo(AbstractConfigItem o) { + if (o instanceof ConfigSection) { + return -1; + } + + ConfigProperty other = (ConfigProperty) o; + + if (isWithinMap()) { + if (other.isWithinMap()) { + return ConfigPhase.COMPARATOR.compare(phase, other.getPhase()); + } + return 1; + } else if (other.isWithinMap()) { + return -1; + } + + return ConfigPhase.COMPARATOR.compare(phase, other.getPhase()); + } + + @Override + public boolean hasDurationType() { + return Duration.class.getName().equals(type); + } + + @Override + public boolean hasMemorySizeType() { + return Types.MEMORY_SIZE_TYPE.equals(type); + } + + @Override + protected void walk(ConfigItemVisitor visitor) { + visitor.visit(this); + } + + public record PropertyPath(String property, String environmentVariable) implements Path { + + @Override + public String toString() { + return property(); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigRoot.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigRoot.java new file mode 100644 index 00000000000000..c4d6096d475b56 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigRoot.java @@ -0,0 +1,140 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +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; +import java.util.regex.Pattern; + +import io.quarkus.annotation.processor.documentation.config.util.Markers; + +/** + * At this stage, a config root is actually a prefix: we merged all the config roots with the same prefix. + *

+ * Thus the phase for instance is not stored at this level but at the item level. + */ +public class ConfigRoot implements ConfigItemCollection { + + private final Extension extension; + private final String prefix; + // used by the doc generation to classify config roots + private final String topLevelPrefix; + + private final String overriddenDocFileName; + private final List items = new ArrayList<>(); + private final Set qualifiedNames = new HashSet<>(); + + public ConfigRoot(Extension extension, String prefix, String overriddenDocPrefix, String overriddenDocFileName) { + this.extension = extension; + this.prefix = prefix; + this.overriddenDocFileName = overriddenDocFileName; + this.topLevelPrefix = overriddenDocPrefix != null ? buildTopLevelPrefix(overriddenDocPrefix) + : buildTopLevelPrefix(prefix); + } + + public Extension getExtension() { + return extension; + } + + public String getPrefix() { + return prefix; + } + + public String getOverriddenDocFileName() { + return overriddenDocFileName; + } + + public void addQualifiedName(String qualifiedName) { + qualifiedNames.add(qualifiedName); + } + + public Set getQualifiedNames() { + return Collections.unmodifiableSet(qualifiedNames); + } + + @Override + public void addItem(AbstractConfigItem item) { + this.items.add(item); + } + + @Override + public List getItems() { + return Collections.unmodifiableList(items); + } + + public String getTopLevelPrefix() { + return topLevelPrefix; + } + + public void merge(ConfigRoot other) { + this.qualifiedNames.addAll(other.getQualifiedNames()); + + Map existingConfigSections = new HashMap<>(); + collectConfigSections(existingConfigSections, this); + + for (AbstractConfigItem otherItem : other.getItems()) { + if (otherItem instanceof ConfigSection otherConfigSection) { + ConfigSection similarConfigSection = existingConfigSections.get(otherConfigSection.getPath().property()); + + if (similarConfigSection == null) { + this.items.add(otherConfigSection); + } else { + similarConfigSection.merge(otherConfigSection, existingConfigSections); + } + } else if (otherItem instanceof ConfigProperty configProperty) { + this.items.add(configProperty); + } else { + throw new IllegalStateException("Unknown item type: " + otherItem.getClass()); + } + } + + Collections.sort(this.items); + } + + private void collectConfigSections(Map configSections, ConfigItemCollection configItemCollection) { + for (AbstractConfigItem item : configItemCollection.getItems()) { + if (item instanceof ConfigSection configSection) { + configSections.put(item.getPath().property(), configSection); + + collectConfigSections(configSections, configSection); + } + } + } + + public boolean hasDurationType() { + for (AbstractConfigItem item : items) { + if (item.hasDurationType() && !item.isDeprecated()) { + return true; + } + } + return false; + } + + public boolean hasMemorySizeType() { + for (AbstractConfigItem item : items) { + if (item.hasMemorySizeType() && !item.isDeprecated()) { + return true; + } + } + return false; + } + + private static String buildTopLevelPrefix(String prefix) { + String[] prefixSegments = prefix.split(Pattern.quote(Markers.DOT)); + + if (prefixSegments.length == 1) { + return prefixSegments[0]; + } + + return prefixSegments[0] + Markers.DOT + prefixSegments[1]; + } + + public void walk(ConfigItemVisitor visitor) { + for (AbstractConfigItem item : items) { + item.walk(visitor); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigSection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigSection.java new file mode 100644 index 00000000000000..da33958996e84e --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigSection.java @@ -0,0 +1,129 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public final class ConfigSection extends AbstractConfigItem implements ConfigItemCollection { + + private boolean generated; + private final List items = new ArrayList<>(); + private final int level; + + public ConfigSection(String sourceClass, String sourceName, SourceType sourceType, SectionPath path, String type, int level, + boolean generated, Deprecation deprecation) { + super(sourceClass, sourceName, sourceType, path, type, deprecation); + this.generated = generated; + this.level = level; + } + + @Override + public void addItem(AbstractConfigItem item) { + this.items.add(item); + } + + @Override + public List getItems() { + return Collections.unmodifiableList(items); + } + + @Override + public int compareTo(AbstractConfigItem o) { + if (o instanceof ConfigProperty) { + return 1; + } + + return 0; + } + + public SectionPath getPath() { + return (SectionPath) super.getPath(); + } + + public boolean isSection() { + return true; + } + + public boolean isGenerated() { + return generated; + } + + public int getLevel() { + return level; + } + + /** + * This is used when we merge ConfigSection at the ConfigRoot level. + * It can happen when for instance a path is both used at a given level and in an unnamed map. + * For instance in: HibernateOrmConfig. + */ + public void appendState(boolean generated, Deprecation deprecation) { + // we generate the section if at least one of the sections should be generated + // (the output will contain all the items of the section) + this.generated = this.generated || generated; + // we unmark the section as deprecated if one of the merged section is not deprecated + // as we will have to generate the section + this.deprecation = this.deprecation != null && deprecation != null ? this.deprecation : null; + } + + /** + * This is used to merge ConfigRoot when generating the AsciiDoc output. + */ + public void merge(ConfigSection other, Map existingConfigSections) { + this.generated = this.generated || other.generated; + + for (AbstractConfigItem otherItem : other.getItems()) { + if (otherItem instanceof ConfigSection otherConfigSection) { + ConfigSection similarConfigSection = existingConfigSections.get(otherConfigSection.getPath().property()); + if (similarConfigSection == null) { + this.items.add(otherConfigSection); + } else { + similarConfigSection.merge(otherConfigSection, existingConfigSections); + } + } else if (otherItem instanceof ConfigProperty configProperty) { + this.items.add(configProperty); + } else { + throw new IllegalStateException("Unknown item type: " + otherItem.getClass()); + } + } + + Collections.sort(this.items); + } + + @Override + public boolean hasDurationType() { + for (AbstractConfigItem item : items) { + if (item.hasDurationType() && !item.isDeprecated()) { + return true; + } + } + return false; + } + + @Override + public boolean hasMemorySizeType() { + for (AbstractConfigItem item : items) { + if (item.hasMemorySizeType() && !item.isDeprecated()) { + return true; + } + } + return false; + } + + @Override + protected void walk(ConfigItemVisitor visitor) { + visitor.visit(this); + for (AbstractConfigItem item : items) { + item.walk(visitor); + } + } + + public record SectionPath(String property) implements Path { + + @Override + public String toString() { + return property(); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/Deprecation.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/Deprecation.java new file mode 100644 index 00000000000000..2f8b4e18fee302 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/Deprecation.java @@ -0,0 +1,5 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +public record Deprecation(String since, String replacement, String reason) { + +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/EnumAcceptedValues.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/EnumAcceptedValues.java new file mode 100644 index 00000000000000..c49ec625afbf63 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/EnumAcceptedValues.java @@ -0,0 +1,12 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.util.Map; + +/** + * This is the enum accepted values that will appear in the documentation. + */ +public record EnumAcceptedValues(String qualifiedName, Map values) { + + public record EnumAcceptedValue(String configValue) { + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/Extension.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/Extension.java new file mode 100644 index 00000000000000..fcb44039f24dec --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/Extension.java @@ -0,0 +1,88 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public record Extension(String groupId, String artifactId, String name, + NameSource nameSource, boolean detected) implements Comparable { + + public static Extension createNotDetected() { + return new Extension("not.detected", "not.detected", "Not detected", NameSource.NONE, false); + } + + @Override + public final String toString() { + return groupId + ":" + artifactId; + } + + @Override + public int hashCode() { + return Objects.hash(artifactId, groupId); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Extension other = (Extension) obj; + return Objects.equals(artifactId, other.artifactId) && Objects.equals(groupId, other.groupId); + } + + // TODO #42114 remove once fixed + @Deprecated(forRemoval = true) + @JsonIgnore + public boolean isMixedModule() { + return "io.quarkus".equals(groupId) && ("quarkus-core".equals(artifactId) || "quarkus-messaging".equals(artifactId)); + } + + @JsonIgnore + public boolean splitOnConfigRootDescription() { + // quarkus-core has a lot of config roots and they are very specific + // we need to split them properly in the generated documentation + return "io.quarkus".equals(groupId) && "quarkus-core".equals(artifactId); + } + + @Override + public int compareTo(Extension other) { + if (name != null && other.name != null) { + int nameComparison = name.compareToIgnoreCase(other.name); + if (nameComparison != 0) { + return nameComparison; + } + } + + int groupIdComparison = groupId.compareToIgnoreCase(other.groupId); + if (groupIdComparison != 0) { + return groupIdComparison; + } + + return artifactId.compareToIgnoreCase(other.artifactId); + } + + public static enum NameSource { + + EXTENSION_METADATA(100), + EXTENSION_METADATA_COMMON_INTERNAL(90), + POM_XML(50), + POM_XML_COMMON_INTERNAL(40), + NONE(-1); + + private final int priority; + + NameSource(int priority) { + this.priority = priority; + } + + public boolean isBetterThan(NameSource other) { + return this.priority > other.priority; + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java new file mode 100644 index 00000000000000..8a3db353d4c9fe --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java @@ -0,0 +1,13 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.util.Map; + +public record JavadocElements(Extension extension, Map elements) { + + public record JavadocElement(String description, JavadocFormat format, String since, String deprecated) { + } + + public boolean isEmpty() { + return elements.isEmpty(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java new file mode 100644 index 00000000000000..b44020aa4ebb1e --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java @@ -0,0 +1,8 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +public enum JavadocFormat { + + ASCIIDOC, + MARKDOWN, + JAVADOC; +} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ResolvedModel.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ResolvedModel.java new file mode 100644 index 00000000000000..0c0c24d3aeb218 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ResolvedModel.java @@ -0,0 +1,46 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * This is the fully resolved model for a given module. + *

+ * This model doesn't contain the Javadoc: the Javadoc is generated per module and can't be part of the model + * as when referencing a ConfigGroup that is outside of the boundaries of the module, the Javadoc is not available. + *

+ * The model is fully resolved though as all the config annotations have a runtime retention so, even if the source + * is not available in the module, we can resolve all the annotations and the model. + *

+ * It is the responsibility of the model consumer to assemble the config roots (if needed) and to get the Javadoc from the files + * containing it. + */ +public class ResolvedModel { + + /** + * List of config roots: note that at this point they are not merged: you have one object per {@code @ConfigRoot} + * annotation. + */ + private List configRoots; + + @JsonCreator + public ResolvedModel(List configRoots) { + this.configRoots = configRoots == null ? List.of() : Collections.unmodifiableList(configRoots); + } + + public List getConfigRoots() { + return configRoots; + } + + public void walk(ConfigItemVisitor visitor) { + for (ConfigRoot configRoot : configRoots) { + configRoot.walk(visitor); + } + } + + public boolean isEmpty() { + return configRoots.isEmpty(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/SourceType.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/SourceType.java new file mode 100644 index 00000000000000..6da161b75714e2 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/SourceType.java @@ -0,0 +1,7 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +public enum SourceType { + + METHOD, + FIELD; +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/resolver/ConfigResolver.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/resolver/ConfigResolver.java new file mode 100644 index 00000000000000..d6fa963d6dd12a --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/resolver/ConfigResolver.java @@ -0,0 +1,314 @@ +package io.quarkus.annotation.processor.documentation.config.resolver; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigGroup; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigProperty; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; +import io.quarkus.annotation.processor.documentation.config.discovery.EnumDefinition; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; +import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; +import io.quarkus.annotation.processor.documentation.config.model.ConfigPhase; +import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; +import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty.PropertyPath; +import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection.SectionPath; +import io.quarkus.annotation.processor.documentation.config.model.Deprecation; +import io.quarkus.annotation.processor.documentation.config.model.EnumAcceptedValues; +import io.quarkus.annotation.processor.documentation.config.model.EnumAcceptedValues.EnumAcceptedValue; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.ResolvedModel; +import io.quarkus.annotation.processor.documentation.config.scanner.ConfigCollector; +import io.quarkus.annotation.processor.documentation.config.util.ConfigNamingUtil; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; +import io.quarkus.annotation.processor.documentation.config.util.Markers; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Strings; +import io.quarkus.annotation.processor.util.Utils; + +/** + * The goal of this class is to resolve the elements obtained on scanning/discovery + * and assemble them into the final model. + *

+ * Note that the model is not exactly final as some elements might not be resolvable + * because they are inside another module: this annotation processor doesn't cross + * the module boundaries as it causes a lot of headaches (for instance for the Develocity + * caching but not only). + *

+ * NEVER CROSS THE STREAMS! + */ +public class ConfigResolver { + + private final Config config; + private final Utils utils; + private final ConfigCollector configCollector; + + public ConfigResolver(Config config, Utils utils, ConfigCollector configCollector) { + this.config = config; + this.utils = utils; + this.configCollector = configCollector; + } + + public JavadocElements resolveJavadoc() { + return new JavadocElements(config.getExtension(), configCollector.getJavadocElements()); + } + + public ResolvedModel resolveModel() { + List configRoots = new ArrayList<>(); + + for (DiscoveryConfigRoot discoveryConfigRoot : configCollector.getConfigRoots()) { + ConfigRoot configRoot = new ConfigRoot(discoveryConfigRoot.getExtension(), discoveryConfigRoot.getPrefix(), + discoveryConfigRoot.getOverriddenDocPrefix(), discoveryConfigRoot.getOverriddenDocFileName()); + Map existingRootConfigSections = new HashMap<>(); + + configRoot.addQualifiedName(discoveryConfigRoot.getQualifiedName()); + + ResolutionContext context = new ResolutionContext(configRoot.getPrefix(), new ArrayList<>(), discoveryConfigRoot, + configRoot, 0, false, false, null); + for (DiscoveryConfigProperty discoveryConfigProperty : discoveryConfigRoot.getProperties().values()) { + resolveProperty(configRoot, existingRootConfigSections, discoveryConfigRoot.getPhase(), context, + discoveryConfigProperty); + } + + configRoots.add(configRoot); + } + + return new ResolvedModel(configRoots); + } + + private void resolveProperty(ConfigRoot configRoot, Map existingRootConfigSections, + ConfigPhase phase, ResolutionContext context, DiscoveryConfigProperty discoveryConfigProperty) { + String path = appendPath(context.getPath(), discoveryConfigProperty.getPath()); + + List additionalPaths = context.getAdditionalPaths().stream() + .map(p -> appendPath(p, discoveryConfigProperty.getPath())) + .collect(Collectors.toCollection(ArrayList::new)); + Deprecation deprecation = discoveryConfigProperty.getDeprecation() != null ? discoveryConfigProperty.getDeprecation() + : context.getDeprecation(); + + String typeQualifiedName = discoveryConfigProperty.getType().qualifiedName(); + + if (configCollector.isResolvedConfigGroup(typeQualifiedName)) { + DiscoveryConfigGroup discoveryConfigGroup = configCollector.getResolvedConfigGroup(typeQualifiedName); + + String potentiallyMappedPath = path; + if (discoveryConfigProperty.getType().isMap()) { + if (discoveryConfigProperty.isUnnamedMapKey()) { + ListIterator additionalPathsIterator = additionalPaths.listIterator(); + + additionalPathsIterator + .add(path + ConfigNamingUtil.getMapKey(discoveryConfigProperty.getMapKey())); + while (additionalPathsIterator.hasNext()) { + additionalPathsIterator.add(additionalPathsIterator.next() + + ConfigNamingUtil.getMapKey(discoveryConfigProperty.getMapKey())); + } + } else { + potentiallyMappedPath += ConfigNamingUtil.getMapKey(discoveryConfigProperty.getMapKey()); + additionalPaths = additionalPaths.stream() + .map(p -> p + ConfigNamingUtil.getMapKey(discoveryConfigProperty.getMapKey())) + .collect(Collectors.toCollection(ArrayList::new)); + } + } + + ResolutionContext configGroupContext; + + boolean isWithinMap = context.isWithinMap() || discoveryConfigProperty.getType().isMap(); + boolean isWithMapWithUnnamedKey = context.isWithinMapWithUnnamedKey() || discoveryConfigProperty.isUnnamedMapKey(); + + if (discoveryConfigProperty.isSection()) { + ConfigSection configSection = existingRootConfigSections.get(path); + + if (configSection != null) { + configSection.appendState(discoveryConfigProperty.isSectionGenerated(), deprecation); + } else { + configSection = new ConfigSection(discoveryConfigProperty.getSourceClass(), + discoveryConfigProperty.getSourceName(), discoveryConfigProperty.getSourceType(), + new SectionPath(path), typeQualifiedName, + context.getSectionLevel(), discoveryConfigProperty.isSectionGenerated(), deprecation); + context.getItemCollection().addItem(configSection); + existingRootConfigSections.put(path, configSection); + } + + configGroupContext = new ResolutionContext(potentiallyMappedPath, additionalPaths, discoveryConfigGroup, + configSection, context.getSectionLevel() + 1, isWithinMap, isWithMapWithUnnamedKey, deprecation); + } else { + configGroupContext = new ResolutionContext(potentiallyMappedPath, additionalPaths, discoveryConfigGroup, + context.getItemCollection(), context.getSectionLevel(), isWithinMap, isWithMapWithUnnamedKey, + deprecation); + } + + for (DiscoveryConfigProperty configGroupProperty : discoveryConfigGroup.getProperties().values()) { + resolveProperty(configRoot, existingRootConfigSections, phase, configGroupContext, configGroupProperty); + } + } else { + String typeBinaryName = discoveryConfigProperty.getType().binaryName(); + String typeSimplifiedName = discoveryConfigProperty.getType().simplifiedName(); + + // if the property has a converter, we don't hyphenate the values (per historical rules, not exactly sure of the reason) + boolean hyphenateEnumValues = discoveryConfigProperty.isEnforceHyphenateEnumValue() || + !discoveryConfigProperty.isConverted(); + + String defaultValue = getDefaultValue(discoveryConfigProperty.getDefaultValue(), + discoveryConfigProperty.getDefaultValueForDoc(), discoveryConfigProperty.getType(), hyphenateEnumValues); + + EnumAcceptedValues enumAcceptedValues = null; + if (discoveryConfigProperty.getType().isEnum()) { + EnumDefinition enumDefinition = configCollector.getResolvedEnum(typeQualifiedName); + Map localAcceptedValues = enumDefinition.constants().entrySet().stream() + .collect(Collectors.toMap( + e -> e.getKey(), + e -> new EnumAcceptedValue(e.getValue().hasExplicitValue() ? e.getValue().explicitValue() + : (hyphenateEnumValues ? ConfigNamingUtil.hyphenateEnumValue(e.getKey()) + : e.getKey())), + (x, y) -> y, LinkedHashMap::new)); + enumAcceptedValues = new EnumAcceptedValues(enumDefinition.qualifiedName(), localAcceptedValues); + } + + String potentiallyMappedPath = path; + boolean optional = discoveryConfigProperty.getType().isOptional(); + + if (discoveryConfigProperty.getType().isMap()) { + // it is a leaf pass through map, it is always optional + optional = true; + typeQualifiedName = utils.element().getQualifiedName(discoveryConfigProperty.getType().wrapperType()); + typeSimplifiedName = utils.element().simplifyGenericType(discoveryConfigProperty.getType().wrapperType()); + + potentiallyMappedPath += ConfigNamingUtil.getMapKey(discoveryConfigProperty.getMapKey()); + additionalPaths = additionalPaths.stream() + .map(p -> p + ConfigNamingUtil.getMapKey(discoveryConfigProperty.getMapKey())) + .collect(Collectors.toCollection(ArrayList::new)); + } else if (discoveryConfigProperty.getType().isList()) { + typeQualifiedName = utils.element().getQualifiedName(discoveryConfigProperty.getType().wrapperType()); + } + + PropertyPath propertyPath = new PropertyPath(potentiallyMappedPath, + ConfigNamingUtil.toEnvVarName(potentiallyMappedPath)); + List additionalPropertyPaths = additionalPaths.stream() + .map(ap -> new PropertyPath(ap, ConfigNamingUtil.toEnvVarName(ap))) + .toList(); + + // this is a standard property + ConfigProperty configProperty = new ConfigProperty(phase, + discoveryConfigProperty.getSourceClass(), + discoveryConfigProperty.getSourceName(), + discoveryConfigProperty.getSourceType(), + propertyPath, additionalPropertyPaths, + typeQualifiedName, typeSimplifiedName, + discoveryConfigProperty.getType().isMap(), discoveryConfigProperty.getType().isList(), + optional, discoveryConfigProperty.getMapKey(), + discoveryConfigProperty.isUnnamedMapKey(), context.isWithinMap(), + discoveryConfigProperty.isConverted(), + discoveryConfigProperty.getType().isEnum(), + enumAcceptedValues, defaultValue, + JavadocUtil.getJavadocSiteLink(typeBinaryName), + deprecation); + context.getItemCollection().addItem(configProperty); + } + } + + public static String getDefaultValue(String defaultValue, String defaultValueForDoc, ResolvedType type, + boolean hyphenateEnumValues) { + if (!Strings.isBlank(defaultValueForDoc)) { + return defaultValueForDoc; + } + + if (defaultValue == null) { + return null; + } + + if (type.isEnum() && hyphenateEnumValues) { + if (type.isList()) { + return Arrays.stream(defaultValue.split(Markers.COMMA)) + .map(v -> ConfigNamingUtil.hyphenateEnumValue(v.trim())) + .collect(Collectors.joining(Markers.COMMA)); + } else { + return ConfigNamingUtil.hyphenateEnumValue(defaultValue.trim()); + } + } + + return defaultValue; + } + + public static String getType(TypeMirror typeMirror) { + if (typeMirror instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) typeMirror; + TypeElement typeElement = (TypeElement) declaredType.asElement(); + return typeElement.getQualifiedName().toString(); + } + return typeMirror.toString(); + } + + public static String appendPath(String parentPath, String path) { + return Markers.PARENT.equals(path) ? parentPath : parentPath + Markers.DOT + path; + } + + private static class ResolutionContext { + + private final String path; + private final List additionalPaths; + private final DiscoveryRootElement discoveryRootElement; + private final ConfigItemCollection itemCollection; + private final int sectionLevel; + private final boolean withinMap; + private final boolean withinMapWithUnnamedKey; + private final Deprecation deprecation; + + private ResolutionContext(String path, List additionalPaths, DiscoveryRootElement discoveryRootElement, + ConfigItemCollection itemCollection, + int sectionLevel, boolean withinMap, boolean withinMapWithUnnamedKey, Deprecation deprecation) { + this.path = path; + this.additionalPaths = additionalPaths; + this.discoveryRootElement = discoveryRootElement; + this.itemCollection = itemCollection; + this.withinMap = withinMap; + this.withinMapWithUnnamedKey = withinMapWithUnnamedKey; + this.deprecation = deprecation; + this.sectionLevel = sectionLevel; + } + + public String getPath() { + return path; + } + + public List getAdditionalPaths() { + return additionalPaths; + } + + public DiscoveryRootElement getDiscoveryRootElement() { + return discoveryRootElement; + } + + public ConfigItemCollection getItemCollection() { + return itemCollection; + } + + public int getSectionLevel() { + return sectionLevel; + } + + public boolean isWithinMap() { + return withinMap; + } + + public boolean isWithinMapWithUnnamedKey() { + return withinMapWithUnnamedKey; + } + + public Deprecation getDeprecation() { + return deprecation; + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractConfigListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractConfigListener.java new file mode 100644 index 00000000000000..4ab20e4ee24e33 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractConfigListener.java @@ -0,0 +1,99 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigGroup; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigProperty; +import io.quarkus.annotation.processor.documentation.config.discovery.EnumDefinition; +import io.quarkus.annotation.processor.documentation.config.discovery.EnumDefinition.EnumConstant; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +public class AbstractConfigListener implements ConfigAnnotationListener { + + protected final Config config; + protected final Utils utils; + protected final ConfigCollector configCollector; + + protected AbstractConfigListener(Config config, Utils utils, ConfigCollector configCollector) { + this.config = config; + this.utils = utils; + this.configCollector = configCollector; + } + + @Override + public Optional onConfigGroup(TypeElement configGroup) { + DiscoveryConfigGroup discoveryConfigGroup = new DiscoveryConfigGroup(config.getExtension(), + utils.element().getBinaryName(configGroup), + configGroup.getQualifiedName().toString(), + // interface config groups are considered config mappings, let's hope it's enough + configGroup.getKind() == ElementKind.INTERFACE); + configCollector.addResolvedConfigGroup(discoveryConfigGroup); + return Optional.of(discoveryConfigGroup); + } + + @Override + public void onResolvedEnum(TypeElement enumTypeElement) { + Map enumConstants = new LinkedHashMap<>(); + + for (Element enumElement : enumTypeElement.getEnclosedElements()) { + if (enumElement.getKind() != ElementKind.ENUM_CONSTANT) { + continue; + } + + String explicitValue = null; + Map annotations = utils.element().getAnnotations(enumElement); + AnnotationMirror configDocEnumValue = annotations.get(Types.ANNOTATION_CONFIG_DOC_ENUM_VALUE); + if (configDocEnumValue != null) { + Map enumValueValues = utils.element().getAnnotationValues(configDocEnumValue); + explicitValue = (String) enumValueValues.get("value"); + } + + enumConstants.put(enumElement.getSimpleName().toString(), new EnumConstant(explicitValue)); + } + + EnumDefinition enumDefinition = new EnumDefinition(enumTypeElement.getQualifiedName().toString(), + enumConstants); + configCollector.addResolvedEnum(enumDefinition); + } + + protected void handleCommonPropertyAnnotations(DiscoveryConfigProperty.Builder builder, + Map propertyAnnotations, ResolvedType resolvedType, String sourceName) { + + AnnotationMirror deprecatedAnnotation = propertyAnnotations.get(Deprecated.class.getName()); + if (deprecatedAnnotation != null) { + String since = (String) utils.element().getAnnotationValues(deprecatedAnnotation).get("since"); + // TODO add more information about the deprecation, typically the reason and a replacement + builder.deprecated(since, null, null); + } + + AnnotationMirror configDocSectionAnnotation = propertyAnnotations.get(Types.ANNOTATION_CONFIG_DOC_SECTION); + if (configDocSectionAnnotation != null) { + Boolean sectionGenerated = (Boolean) utils.element().getAnnotationValues(configDocSectionAnnotation) + .get("generated"); + if (sectionGenerated != null && sectionGenerated) { + builder.section(true); + } else { + builder.section(false); + } + } + + AnnotationMirror configDocEnum = propertyAnnotations.get(Types.ANNOTATION_CONFIG_DOC_ENUM); + if (configDocEnum != null) { + Boolean enforceHyphenateValues = (Boolean) utils.element().getAnnotationValues(configDocEnum) + .get("enforceHyphenateValues"); + if (enforceHyphenateValues != null && enforceHyphenateValues) { + builder.enforceHyphenateEnumValues(); + } + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java new file mode 100644 index 00000000000000..a2fe0e875223a2 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java @@ -0,0 +1,87 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import java.util.Optional; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; +import io.quarkus.annotation.processor.documentation.config.util.Markers; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +public class AbstractJavadocConfigListener implements ConfigAnnotationListener { + + protected final Config config; + protected final Utils utils; + protected final ConfigCollector configCollector; + + protected AbstractJavadocConfigListener(Config config, Utils utils, ConfigCollector configCollector) { + this.config = config; + this.utils = utils; + this.configCollector = configCollector; + } + + @Override + public Optional onConfigRoot(TypeElement configRoot) { + // we only get Javadoc for local classes + // classes coming from other modules won't have Javadoc available + if (!utils.element().isLocalClass(configRoot)) { + return Optional.empty(); + } + + Optional rawJavadoc = utils.element().getJavadoc(configRoot); + + if (rawJavadoc.isEmpty()) { + return Optional.empty(); + } + + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc.get()); + if (parsedJavadocSection.title() == null) { + return Optional.empty(); + } + + configCollector.addJavadocElement( + configRoot.getQualifiedName().toString(), + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); + + return Optional.empty(); + } + + @Override + public void onResolvedEnum(TypeElement enumTypeElement) { + if (!utils.element().isLocalClass(enumTypeElement)) { + return; + } + + for (Element enumElement : enumTypeElement.getEnclosedElements()) { + if (enumElement.getKind() != ElementKind.ENUM_CONSTANT) { + continue; + } + + Optional rawJavadoc = utils.element().getJavadoc(enumElement); + + if (rawJavadoc.isEmpty()) { + continue; + } + + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc.get()); + + if (parsedJavadoc.description() == null) { + continue; + } + + configCollector.addJavadocElement( + enumTypeElement.getQualifiedName().toString() + Markers.DOT + enumElement.getSimpleName() + .toString(), + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationListener.java new file mode 100644 index 00000000000000..4a88e07f4e790a --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationListener.java @@ -0,0 +1,43 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import java.util.Optional; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigGroup; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; + +public interface ConfigAnnotationListener { + + default Optional onConfigRoot(TypeElement configRoot) { + return Optional.empty(); + } + + default void onSuperclass(DiscoveryRootElement discoveryRootElement, TypeElement superClass) { + } + + default void onInterface(DiscoveryRootElement discoveryRootElement, TypeElement interfaze) { + } + + default Optional onConfigGroup(TypeElement configGroup) { + return Optional.empty(); + } + + default void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElement clazz, ExecutableElement method, + ResolvedType type) { + } + + default void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeElement clazz, VariableElement field, + ResolvedType type) { + } + + default void onResolvedEnum(TypeElement enumTypeElement) { + } + + default void finalizeProcessing() { + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationScanner.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationScanner.java new file mode 100644 index 00000000000000..0ee2a08b30455e --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationScanner.java @@ -0,0 +1,516 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import static javax.lang.model.util.ElementFilter.typesIn; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic; +import javax.tools.Diagnostic.Kind; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigGroup; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; +import io.quarkus.annotation.processor.documentation.config.model.ConfigPhase; +import io.quarkus.annotation.processor.documentation.config.util.TypeUtil; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +public class ConfigAnnotationScanner { + + private final Utils utils; + private final Config config; + private final ConfigCollector configCollector; + private final Set configGroupClassNames = new HashSet<>(); + private final Set configRootClassNames = new HashSet<>(); + private final Set configMappingWithoutConfigRootClassNames = new HashSet<>(); + private final Set enumClassNames = new HashSet<>(); + + private final List configRootListeners; + + /** + * These are handled specifically as we just want to collect the javadoc. + * They are actually consumed as super interfaces in a config root. + */ + private final List configMappingWithoutConfigRootListeners; + + public ConfigAnnotationScanner(Config config, Utils utils) { + this.config = config; + this.utils = utils; + this.configCollector = new ConfigCollector(); + + List configRootListeners = new ArrayList<>(); + List configMappingWithoutConfigRootListeners = new ArrayList<>(); + + if (!config.getExtension().isMixedModule()) { + // This is what we aim for. We have an exception for Quarkus Core and Quarkus Messaging though. + if (config.useConfigMapping()) { + configRootListeners.add(new JavadocConfigMappingListener(config, utils, configCollector)); + configRootListeners.add(new ConfigMappingListener(config, utils, configCollector)); + + configMappingWithoutConfigRootListeners.add(new JavadocConfigMappingListener(config, utils, configCollector)); + } else { + configRootListeners.add(new JavadocLegacyConfigRootListener(config, utils, configCollector)); + configRootListeners.add(new LegacyConfigRootListener(config, utils, configCollector)); + } + } else { + // TODO #42114 remove once fixed + // we handle both traditional config roots and config mappings + if (config.getExtension().isMixedModule()) { + configRootListeners.add(new JavadocConfigMappingListener(config, utils, configCollector)); + configRootListeners.add(new JavadocLegacyConfigRootListener(config, utils, configCollector)); + configRootListeners.add(new ConfigMappingListener(config, utils, configCollector)); + configRootListeners.add(new LegacyConfigRootListener(config, utils, configCollector)); + + configMappingWithoutConfigRootListeners.add(new JavadocConfigMappingListener(config, utils, configCollector)); + } + } + + this.configRootListeners = Collections.unmodifiableList(configRootListeners); + this.configMappingWithoutConfigRootListeners = Collections.unmodifiableList(configMappingWithoutConfigRootListeners); + } + + public void scanConfigGroups(RoundEnvironment roundEnv, TypeElement annotation) { + for (TypeElement configGroup : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { + if (isConfigGroupAlreadyHandled(configGroup)) { + continue; + } + + debug("Detected annotated config group: " + configGroup, configGroup); + + try { + DiscoveryConfigGroup discoveryConfigGroup = applyRootListeners(l -> l.onConfigGroup(configGroup)); + scanElement(configRootListeners, discoveryConfigGroup, configGroup); + } catch (Exception e) { + throw new IllegalStateException("Unable to scan config group: " + configGroup, e); + } + } + } + + public void scanConfigRoots(RoundEnvironment roundEnv, TypeElement annotation) { + for (TypeElement configRoot : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { + checkConfigRootAnnotationConsistency(configRoot); + + final PackageElement pkg = utils.element().getPackageOf(configRoot); + if (pkg == null) { + utils.processingEnv().getMessager().printMessage(Diagnostic.Kind.ERROR, + "Element " + configRoot + " has no enclosing package"); + continue; + } + + if (isConfigRootAlreadyHandled(configRoot)) { + continue; + } + + debug("Detected config root: " + configRoot, configRoot); + + try { + DiscoveryConfigRoot discoveryConfigRoot = applyRootListeners(l -> l.onConfigRoot(configRoot)); + scanElement(configRootListeners, discoveryConfigRoot, configRoot); + } catch (Exception e) { + throw new IllegalStateException("Unable to scan config root: " + configRoot, e); + } + } + } + + /** + * In this case, we will just apply the Javadoc listeners to collect Javadoc. + */ + public void scanConfigMappingsWithoutConfigRoot(RoundEnvironment roundEnv, TypeElement annotation) { + for (TypeElement configMappingWithoutConfigRoot : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { + if (utils.element().isAnnotationPresent(configMappingWithoutConfigRoot, Types.ANNOTATION_CONFIG_ROOT)) { + continue; + } + + final PackageElement pkg = utils.element().getPackageOf(configMappingWithoutConfigRoot); + if (pkg == null) { + utils.processingEnv().getMessager().printMessage(Diagnostic.Kind.ERROR, + "Element " + configMappingWithoutConfigRoot + " has no enclosing package"); + continue; + } + + if (isConfigMappingWithoutConfigRootAlreadyHandled(configMappingWithoutConfigRoot)) { + continue; + } + + debug("Detected config mapping without config root: " + configMappingWithoutConfigRoot, + configMappingWithoutConfigRoot); + + try { + // we need to forge a dummy DiscoveryConfigRoot + // it's mostly ignored in the listeners, except for checking if it's a config mapping (for mixed modules) + DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), "dummy", "dummy", + utils.element().getBinaryName(configMappingWithoutConfigRoot), + configMappingWithoutConfigRoot.getQualifiedName().toString(), + ConfigPhase.BUILD_TIME, null, true); + scanElement(configMappingWithoutConfigRootListeners, discoveryConfigRoot, configMappingWithoutConfigRoot); + } catch (Exception e) { + throw new IllegalStateException( + "Unable to scan config mapping without config root: " + configMappingWithoutConfigRoot, e); + } + } + } + + public ConfigCollector finalizeProcessing() { + applyListeners(configRootListeners, l -> l.finalizeProcessing()); + applyListeners(configMappingWithoutConfigRootListeners, l -> l.finalizeProcessing()); + + return configCollector; + } + + private void scanElement(List listeners, DiscoveryRootElement configRootElement, + TypeElement clazz) { + // we scan the superclass and interfaces first so that the local elements can potentially override them + if (clazz.getKind() == ElementKind.INTERFACE) { + List superInterfaces = clazz.getInterfaces(); + for (TypeMirror superInterface : superInterfaces) { + TypeElement superInterfaceTypeElement = (TypeElement) ((DeclaredType) superInterface).asElement(); + + debug("Detected superinterface: " + superInterfaceTypeElement, clazz); + + applyListeners(listeners, l -> l.onInterface(configRootElement, superInterfaceTypeElement)); + scanElement(listeners, configRootElement, superInterfaceTypeElement); + } + } else { + TypeMirror superclass = clazz.getSuperclass(); + if (superclass.getKind() != TypeKind.NONE + && !utils.element().getQualifiedName(superclass).equals(Object.class.getName())) { + TypeElement superclassTypeElement = (TypeElement) ((DeclaredType) superclass).asElement(); + + debug("Detected superclass: " + superclassTypeElement, clazz); + + applyListeners(listeners, l -> l.onSuperclass(configRootElement, clazz)); + scanElement(listeners, configRootElement, superclassTypeElement); + } + } + + for (Element e : clazz.getEnclosedElements()) { + switch (e.getKind()) { + case INTERFACE: { + // We don't need to catch the enclosed interface anymore + // They are config groups and they will be detected as such when parsing the methods + break; + } + + case METHOD: { + ExecutableElement method = (ExecutableElement) e; + if (isMethodIgnored(method)) { + continue; + } + + // Find groups without annotation + TypeMirror returnType = method.getReturnType(); + + ResolvedType resolvedType = resolveType(returnType); + if (resolvedType.isEnum()) { + handleEnum(listeners, resolvedType.unwrappedTypeElement()); + } else if (resolvedType.isInterface()) { + TypeElement unwrappedTypeElement = resolvedType.unwrappedTypeElement(); + if (!utils.element().isJdkClass(unwrappedTypeElement)) { + if (!isConfigGroupAlreadyHandled(unwrappedTypeElement)) { + debug("Detected config group: " + resolvedType + " on method: " + + method, clazz); + + DiscoveryConfigGroup discoveryConfigGroup = applyRootListeners( + l -> l.onConfigGroup(unwrappedTypeElement)); + scanElement(listeners, discoveryConfigGroup, unwrappedTypeElement); + } + } + } + + debug("Detected enclosed method: " + method, e); + + applyListeners(listeners, l -> l.onEnclosedMethod(configRootElement, clazz, method, resolvedType)); + + break; + } + + case FIELD: { + VariableElement field = (VariableElement) e; + + if (isFieldIgnored(field)) { + continue; + } + + ResolvedType resolvedType = resolveType(field.asType()); + + if (resolvedType.isEnum()) { + handleEnum(listeners, resolvedType.unwrappedTypeElement()); + } else if (resolvedType.isClass()) { + TypeElement unwrappedTypeElement = resolvedType.unwrappedTypeElement(); + if (utils.element().isAnnotationPresent(unwrappedTypeElement, Types.ANNOTATION_CONFIG_GROUP) + && !isConfigGroupAlreadyHandled(unwrappedTypeElement)) { + debug("Detected config group: " + resolvedType + " on field: " + + field, clazz); + + DiscoveryConfigGroup discoveryConfigGroup = applyRootListeners( + l -> l.onConfigGroup(unwrappedTypeElement)); + scanElement(listeners, discoveryConfigGroup, unwrappedTypeElement); + } + } + + debug("Detected enclosed field: " + field, clazz); + + applyListeners(listeners, l -> l.onEnclosedField(configRootElement, clazz, field, resolvedType)); + break; + } + + case ENUM: { + handleEnum(listeners, (TypeElement) e); + break; + } + + default: + // do nothing + break; + } + } + } + + private void handleEnum(List listeners, TypeElement enumTypeElement) { + if (isEnumAlreadyHandled(enumTypeElement)) { + return; + } + + applyListeners(listeners, l -> l.onResolvedEnum(enumTypeElement)); + } + + private boolean isConfigRootAlreadyHandled(TypeElement clazz) { + String qualifiedName = clazz.getQualifiedName().toString(); + + return !configRootClassNames.add(qualifiedName); + } + + private boolean isConfigMappingWithoutConfigRootAlreadyHandled(TypeElement clazz) { + String qualifiedName = clazz.getQualifiedName().toString(); + + return !configMappingWithoutConfigRootClassNames.add(qualifiedName); + } + + private boolean isConfigGroupAlreadyHandled(TypeElement clazz) { + String qualifiedName = clazz.getQualifiedName().toString(); + + return !configGroupClassNames.add(qualifiedName); + } + + private boolean isEnumAlreadyHandled(TypeElement clazz) { + String qualifiedName = clazz.getQualifiedName().toString(); + + return !enumClassNames.add(qualifiedName); + } + + private ResolvedType resolveType(TypeMirror typeMirror) { + if (typeMirror.getKind().isPrimitive()) { + return ResolvedType.ofPrimitive(typeMirror, utils.element().getQualifiedName(typeMirror)); + } + if (typeMirror.getKind() == TypeKind.ARRAY) { + ResolvedType resolvedType = resolveType(((ArrayType) typeMirror).getComponentType()); + return ResolvedType.makeList(typeMirror, resolvedType); + } + + DeclaredType declaredType = (DeclaredType) typeMirror; + TypeElement typeElement = (TypeElement) declaredType.asElement(); + + String qualifiedName = typeElement.getQualifiedName().toString(); + + boolean optional = qualifiedName.startsWith(Optional.class.getName()); + boolean map = qualifiedName.equals(Map.class.getName()); + boolean list = qualifiedName.equals(List.class.getName()) + || qualifiedName.equals(Set.class.getName()); + + List typeArguments = declaredType.getTypeArguments(); + if (!typeArguments.isEmpty()) { + // let's resolve the type + if (typeArguments.size() == 1 && optional) { + return ResolvedType.makeOptional(resolveType(typeArguments.get(0))); + } else if (typeArguments.size() == 1 && list) { + return ResolvedType.makeList(typeMirror, resolveType(typeArguments.get(0))); + } else if (typeArguments.size() == 2 && map) { + return ResolvedType.makeMap(typeMirror, resolveType(typeArguments.get(1))); + } + } + + String binaryName = utils.element().getBinaryName(typeElement); + String simplifiedName = getSimplifiedTypeName(typeElement); + + boolean isInterface = false; + boolean isClass = false; + boolean isEnum = false; + boolean isDuration = false; + boolean isConfigGroup = false; + + if (typeElement.getKind() == ElementKind.ENUM) { + isEnum = true; + } else if (typeElement.getKind() == ElementKind.INTERFACE) { + isInterface = true; + isConfigGroup = utils.element().isAnnotationPresent(typeElement, Types.ANNOTATION_CONFIG_GROUP); + } else if (typeElement.getKind() == ElementKind.CLASS) { + isClass = true; + isDuration = utils.element().getQualifiedName(typeMirror).equals(Duration.class.getName()); + isConfigGroup = utils.element().isAnnotationPresent(typeElement, Types.ANNOTATION_CONFIG_GROUP); + } + + ResolvedType resolvedType = ResolvedType.ofDeclaredType(typeMirror, binaryName, qualifiedName, simplifiedName, + isInterface, isClass, isEnum, isDuration, isConfigGroup); + + // optional can also be present on non wrapper types (e.g. OptionalInt) + if (optional) { + return ResolvedType.makeOptional(resolvedType); + } + + return resolvedType; + } + + private String getSimplifiedTypeName(TypeElement typeElement) { + String qualifiedName = typeElement.getQualifiedName().toString(); + + String typeAlias = TypeUtil.getAlias(qualifiedName); + if (typeAlias != null) { + return typeAlias; + } + if (TypeUtil.isPrimitiveWrapper(qualifiedName)) { + return TypeUtil.unbox(qualifiedName); + } + + return typeElement.getSimpleName().toString(); + } + + public boolean isMethodIgnored(ExecutableElement method) { + // default methods are ignored + if (!method.getModifiers().contains(Modifier.ABSTRACT)) { + return true; + } + if (TypeKind.VOID == method.getReturnType().getKind()) { + return true; + } + // Skip toString method, because mappings can include it and generate it + if (method.getSimpleName().contentEquals("toString") + && method.getParameters().isEmpty()) { + return true; + } + + if (utils.element().isAnnotationPresent(method, Types.ANNOTATION_CONFIG_DOC_IGNORE)) { + return true; + } + + return false; + } + + public boolean isFieldIgnored(VariableElement field) { + if (field.getModifiers().contains(Modifier.STATIC)) { + return true; + } + + Map annotations = utils.element().getAnnotations(field); + + if (annotations.containsKey(Types.ANNOTATION_CONFIG_ITEM)) { + Map annotationValues = utils.element() + .getAnnotationValues(annotations.get(Types.ANNOTATION_CONFIG_ITEM)); + Boolean generateDocumentation = (Boolean) annotationValues.get("generateDocumentation"); + + if (generateDocumentation != null && !generateDocumentation) { + return true; + } + return false; + } + // this was added specifically for @ConfigMapping but it can also be set on fields so let's be safe + if (annotations.containsKey(Types.ANNOTATION_CONFIG_DOC_IGNORE)) { + return true; + } + if (annotations.containsKey(Types.ANNOTATION_CONFIG_DOC_SECTION)) { + return false; + } + + // While I would rather ignore the fields that are not annotated, this is not the current behavior. + // So let's stick to the current behavior. + // See for instance OpenshiftConfig. + return false; + } + + private void applyListeners(List listeners, Consumer listenerFunction) { + for (ConfigAnnotationListener listener : listeners) { + listenerFunction.accept(listener); + } + } + + private T applyRootListeners( + Function> listenerFunction) { + T discoveryRootElement = null; + + for (ConfigAnnotationListener listener : configRootListeners) { + Optional discoveryRootElementCandidate = listenerFunction.apply(listener); + if (discoveryRootElementCandidate.isPresent()) { + if (discoveryRootElement != null) { + throw new IllegalStateException("Multiple listeners returned discovery root elements for: " + + discoveryRootElement.getQualifiedName()); + } + + discoveryRootElement = discoveryRootElementCandidate.get(); + } + } + + if (discoveryRootElement == null) { + throw new IllegalStateException("No listeners returned a discovery root element"); + } + + return discoveryRootElement; + } + + private void checkConfigRootAnnotationConsistency(TypeElement configRoot) { + // for now quarkus-core is a mix of both @ConfigRoot and @ConfigMapping + // see https://github.com/quarkusio/quarkus/issues/42114 + // same for Quarkus Messaging + // TODO #42114 remove once fixed + if (config.getExtension().isMixedModule()) { + return; + } + + if (config.useConfigMapping()) { + if (!utils.element().isAnnotationPresent(configRoot, Types.ANNOTATION_CONFIG_MAPPING)) { + throw new IllegalStateException( + "This module is configured to use @ConfigMapping annotations but we found a @ConfigRoot without a corresponding @ConfigMapping annotation in: " + + configRoot + "." + + " Either add the annotation or add the -AlegacyConfigRoot=true argument to the annotation processor config in the pom.xml"); + } + } else { + if (utils.element().isAnnotationPresent(configRoot, Types.ANNOTATION_CONFIG_MAPPING)) { + throw new IllegalStateException( + "This module is configured to use legacy @ConfigRoot annotations but we found a @ConfigMapping annotation in: " + + configRoot + "." + + " Check the configuration of the annotation processor and drop the -AlegacyConfigRoot=true argument from the pom.xml if needed"); + } + } + } + + private void debug(String debug, Element element) { + if (!config.isDebug()) { + return; + } + + utils.processingEnv().getMessager().printMessage(Kind.NOTE, "[" + element.getSimpleName() + "] " + debug); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigCollector.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigCollector.java new file mode 100644 index 00000000000000..aaeaef3252a142 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigCollector.java @@ -0,0 +1,130 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigGroup; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; +import io.quarkus.annotation.processor.documentation.config.discovery.EnumDefinition; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; + +public class ConfigCollector { + + /** + * Key is qualified name of the class + "." + element name (for instance field or method name) + */ + private Map javadocElements = new HashMap<>(); + + /** + * Key is the qualified name of the class. + */ + private Map configRoots = new HashMap<>(); + + /** + * Key is the qualified name of the class. + */ + private Map resolvedConfigGroups = new HashMap<>(); + + /** + * Key is the qualified name of the class. + */ + private Map resolvedEnums = new HashMap<>(); + + public void addJavadocElement(String key, JavadocElement element) { + javadocElements.put(key, element); + } + + public Map getJavadocElements() { + return Collections.unmodifiableMap(javadocElements); + } + + public void addConfigRoot(DiscoveryConfigRoot configRoot) { + configRoots.put(configRoot.getQualifiedName(), configRoot); + } + + public Collection getConfigRoots() { + return Collections.unmodifiableCollection(configRoots.values()); + } + + public void addResolvedConfigGroup(DiscoveryConfigGroup configGroup) { + resolvedConfigGroups.put(configGroup.getQualifiedName(), configGroup); + } + + public Collection getResolvedConfigGroups() { + return Collections.unmodifiableCollection(resolvedConfigGroups.values()); + } + + public DiscoveryConfigGroup getResolvedConfigGroup(String configGroupClassName) { + return resolvedConfigGroups.get(configGroupClassName); + } + + public boolean isConfigGroup(String className) { + return isResolvedConfigGroup(className); + } + + public boolean isResolvedConfigGroup(String className) { + return resolvedConfigGroups.containsKey(className); + } + + public void addResolvedEnum(EnumDefinition enumDefinition) { + resolvedEnums.put(enumDefinition.qualifiedName(), enumDefinition); + } + + public boolean isEnum(String className) { + return isResolvedEnum(className); + } + + public boolean isResolvedEnum(String className) { + return resolvedEnums.containsKey(className); + } + + public EnumDefinition getResolvedEnum(String name) { + EnumDefinition enumDefinition = resolvedEnums.get(name); + + if (enumDefinition == null) { + throw new IllegalStateException("Could not find registered EnumDefinition for " + name); + } + + return enumDefinition; + } + + public Map getResolvedEnums() { + return resolvedEnums; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("=======================================================\n"); + sb.append("= Config roots\n"); + sb.append("=======================================================\n\n"); + + for (DiscoveryConfigRoot configRoot : configRoots.values()) { + sb.append("- " + configRoot.getQualifiedName() + "\n"); + sb.append(configRoot.toString(" ")); + sb.append("\n\n===\n\n"); + } + if (configRoots.isEmpty()) { + sb.append(" No config roots were detected\n\n"); + } + + sb.append("=======================================================\n"); + sb.append("= Config groups\n"); + sb.append("=======================================================\n\n"); + + for (DiscoveryConfigGroup configGroup : resolvedConfigGroups.values()) { + sb.append("- " + configGroup.getQualifiedName() + "\n"); + sb.append(configGroup.toString(" ")); + sb.append("\n\n===\n\n"); + } + + if (resolvedConfigGroups.isEmpty()) { + sb.append(" No config groups were detected\n\n"); + } + + return sb.toString(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigMappingListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigMappingListener.java new file mode 100644 index 00000000000000..1847c8e2035039 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigMappingListener.java @@ -0,0 +1,186 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import java.util.Map; +import java.util.Optional; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigGroup; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigProperty; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; +import io.quarkus.annotation.processor.documentation.config.model.ConfigPhase; +import io.quarkus.annotation.processor.documentation.config.model.SourceType; +import io.quarkus.annotation.processor.documentation.config.util.ConfigNamingUtil; +import io.quarkus.annotation.processor.documentation.config.util.Markers; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +public class ConfigMappingListener extends AbstractConfigListener { + + ConfigMappingListener(Config config, Utils utils, ConfigCollector configCollector) { + super(config, utils, configCollector); + } + + @Override + public Optional onConfigRoot(TypeElement configRoot) { + if (config.getExtension().isMixedModule() && configRoot.getKind() != ElementKind.INTERFACE) { + return Optional.empty(); + } + + String prefix = Markers.DEFAULT_PREFIX; + ConfigPhase configPhase = ConfigPhase.BUILD_TIME; + + AnnotationMirror configRootAnnotation = null; + AnnotationMirror configMappingAnnotion = null; + AnnotationMirror configDocPrefixAnnotation = null; + AnnotationMirror configDocFileNameAnnotation = null; + + for (AnnotationMirror annotationMirror : configRoot.getAnnotationMirrors()) { + String annotationName = utils.element().getQualifiedName(annotationMirror.getAnnotationType()); + + if (annotationName.equals(Types.ANNOTATION_CONFIG_ROOT)) { + configRootAnnotation = annotationMirror; + continue; + } + if (annotationName.equals(Types.ANNOTATION_CONFIG_MAPPING)) { + configMappingAnnotion = annotationMirror; + continue; + } + if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_PREFIX)) { + configDocPrefixAnnotation = annotationMirror; + continue; + } + if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_FILE_NAME)) { + configDocFileNameAnnotation = annotationMirror; + continue; + } + } + + if (configRootAnnotation == null || configMappingAnnotion == null) { + throw new IllegalStateException("Either @ConfigRoot or @ConfigMapping is missing on " + configRoot); + } + + final Map elementValues = configRootAnnotation + .getElementValues(); + for (Map.Entry entry : elementValues.entrySet()) { + if ("phase()".equals(entry.getKey().toString())) { + configPhase = ConfigPhase.valueOf(entry.getValue().getValue().toString()); + } + } + + for (Map.Entry entry : configMappingAnnotion.getElementValues() + .entrySet()) { + if ("prefix()".equals(entry.getKey().toString())) { + prefix = entry.getValue().getValue().toString(); + } + } + + String overriddenDocPrefix = null; + if (configDocPrefixAnnotation != null) { + for (Map.Entry entry : configDocPrefixAnnotation + .getElementValues() + .entrySet()) { + if ("value()".equals(entry.getKey().toString())) { + overriddenDocPrefix = entry.getValue().getValue().toString(); + break; + } + } + } + + String overriddenDocFileName = null; + if (configDocFileNameAnnotation != null) { + for (Map.Entry entry : configDocFileNameAnnotation + .getElementValues() + .entrySet()) { + if ("value()".equals(entry.getKey().toString())) { + overriddenDocFileName = entry.getValue().getValue().toString(); + break; + } + } + } + + String rootPrefix = ConfigNamingUtil.getRootPrefix(prefix, "", configRoot.getSimpleName().toString(), configPhase); + String binaryName = utils.element().getBinaryName(configRoot); + + DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), + rootPrefix, overriddenDocPrefix, + binaryName, configRoot.getQualifiedName().toString(), configPhase, overriddenDocFileName, true); + configCollector.addConfigRoot(discoveryConfigRoot); + return Optional.of(discoveryConfigRoot); + } + + @Override + public void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElement clazz, ExecutableElement method, + ResolvedType resolvedType) { + if (config.getExtension().isMixedModule() && !discoveryRootElement.isConfigMapping()) { + return; + } + + Map methodAnnotations = utils.element().getAnnotations(method); + + String sourceName = method.getSimpleName().toString(); + DiscoveryConfigProperty.Builder builder = DiscoveryConfigProperty.builder(clazz.getQualifiedName().toString(), + sourceName, SourceType.METHOD, resolvedType); + + String name = ConfigNamingUtil.hyphenate(sourceName); + AnnotationMirror withNameAnnotation = methodAnnotations.get(Types.ANNOTATION_CONFIG_WITH_NAME); + if (withNameAnnotation != null) { + name = withNameAnnotation.getElementValues().values().iterator().next().getValue().toString(); + } + if (methodAnnotations.containsKey(Types.ANNOTATION_CONFIG_WITH_PARENT_NAME)) { + name = Markers.PARENT; + } + builder.name(name); + + AnnotationMirror withDefaultAnnotation = methodAnnotations.get(Types.ANNOTATION_CONFIG_WITH_DEFAULT); + if (withDefaultAnnotation != null) { + builder.defaultValue(withDefaultAnnotation.getElementValues().values().isEmpty() ? null + : withDefaultAnnotation.getElementValues().values().iterator().next().getValue().toString()); + } + + AnnotationMirror configDocDefaultAnnotation = methodAnnotations.get(Types.ANNOTATION_CONFIG_DOC_DEFAULT); + if (configDocDefaultAnnotation != null) { + builder.defaultValueForDoc( + configDocDefaultAnnotation.getElementValues().values().iterator().next().getValue().toString()); + } + + if (resolvedType.isMap()) { + String mapKey = ConfigNamingUtil.hyphenate(sourceName); + AnnotationMirror configDocMapKeyAnnotation = methodAnnotations.get(Types.ANNOTATION_CONFIG_DOC_MAP_KEY); + if (configDocMapKeyAnnotation != null) { + mapKey = configDocMapKeyAnnotation.getElementValues().values().iterator().next().getValue().toString(); + } + builder.mapKey(mapKey); + + AnnotationMirror unnamedMapKeyAnnotation = methodAnnotations.get(Types.ANNOTATION_CONFIG_WITH_UNNAMED_KEY); + if (unnamedMapKeyAnnotation != null) { + builder.unnamedMapKey(); + } + } + + if (methodAnnotations.containsKey(Types.ANNOTATION_CONFIG_WITH_CONVERTER)) { + builder.converted(); + } + + handleCommonPropertyAnnotations(builder, methodAnnotations, resolvedType, sourceName); + + discoveryRootElement.addProperty(builder.build()); + } + + @Deprecated(forRemoval = true) + @Override + public Optional onConfigGroup(TypeElement configGroup) { + if (config.getExtension().isMixedModule() && configGroup.getKind() != ElementKind.INTERFACE) { + return Optional.empty(); + } + + return super.onConfigGroup(configGroup); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java new file mode 100644 index 00000000000000..3a978fd690ea94 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java @@ -0,0 +1,71 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; +import io.quarkus.annotation.processor.documentation.config.util.Markers; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +/** + * This class is responsible for collecting and writing the Javadoc. + */ +public class JavadocConfigMappingListener extends AbstractJavadocConfigListener { + + JavadocConfigMappingListener(Config config, Utils utils, ConfigCollector configCollector) { + super(config, utils, configCollector); + } + + @Override + public void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElement clazz, ExecutableElement method, + ResolvedType resolvedType) { + if (config.getExtension().isMixedModule() && !discoveryRootElement.isConfigMapping()) { + return; + } + + // we only get Javadoc for local classes + // classes coming from other modules won't have Javadoc available + if (!utils.element().isLocalClass(clazz)) { + return; + } + + String rawJavadoc = utils.element().getJavadoc(method).orElse(""); + boolean isSection = utils.element().isAnnotationPresent(method, Types.ANNOTATION_CONFIG_DOC_SECTION); + + if (isSection) { + // for sections, we only keep the title + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc); + + if (parsedJavadocSection.title() == null) { + return; + } + + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); + } else { + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc); + + // We require a Javadoc for config items that are not config groups except if they are a section + if (parsedJavadoc.description() == null) { + if (parsedJavadoc.deprecated() == null && !resolvedType.isConfigGroup()) { + utils.element().addMissingJavadocError(method); + } + return; + } + + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java new file mode 100644 index 00000000000000..638a7bfb4ed4b7 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java @@ -0,0 +1,71 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; +import io.quarkus.annotation.processor.documentation.config.util.Markers; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +/** + * This class is responsible for collecting and writing the Javadoc. + */ +public class JavadocLegacyConfigRootListener extends AbstractJavadocConfigListener { + + JavadocLegacyConfigRootListener(Config config, Utils utils, ConfigCollector configCollector) { + super(config, utils, configCollector); + } + + @Override + public void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeElement clazz, VariableElement field, + ResolvedType resolvedType) { + if (config.getExtension().isMixedModule() && discoveryRootElement.isConfigMapping()) { + return; + } + + // we only get Javbadoc for local classes + // classes coming from other modules won't have Javadoc available + if (!utils.element().isLocalClass(clazz)) { + return; + } + + String rawJavadoc = utils.element().getJavadoc(field).orElse(""); + boolean isSection = utils.element().isAnnotationPresent(field, Types.ANNOTATION_CONFIG_DOC_SECTION); + + if (isSection) { + // for sections, we only keep the title + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc); + + if (parsedJavadocSection.title() == null) { + return; + } + + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); + } else { + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc); + + // We require a Javadoc for config items that are not config groups except if they are a section + if (parsedJavadoc.description() == null) { + if (parsedJavadoc.deprecated() == null && !resolvedType.isConfigGroup()) { + utils.element().addMissingJavadocError(field); + } + return; + } + + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigRootListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigRootListener.java new file mode 100644 index 00000000000000..6a1ca922d3da31 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigRootListener.java @@ -0,0 +1,188 @@ +package io.quarkus.annotation.processor.documentation.config.scanner; + +import java.util.Map; +import java.util.Optional; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; + +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigGroup; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigProperty; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; +import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; +import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; +import io.quarkus.annotation.processor.documentation.config.model.ConfigPhase; +import io.quarkus.annotation.processor.documentation.config.model.SourceType; +import io.quarkus.annotation.processor.documentation.config.util.ConfigNamingUtil; +import io.quarkus.annotation.processor.documentation.config.util.Markers; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Strings; +import io.quarkus.annotation.processor.util.Utils; + +public class LegacyConfigRootListener extends AbstractConfigListener { + + LegacyConfigRootListener(Config config, Utils utils, ConfigCollector configCollector) { + super(config, utils, configCollector); + } + + @Override + public Optional onConfigRoot(TypeElement configRoot) { + if (config.getExtension().isMixedModule() && configRoot.getKind() == ElementKind.INTERFACE) { + return Optional.empty(); + } + + String prefix = Markers.DEFAULT_PREFIX; + ConfigPhase configPhase = ConfigPhase.BUILD_TIME; + + AnnotationMirror configRootAnnotation = null; + AnnotationMirror configDocPrefixAnnotation = null; + AnnotationMirror configDocFileNameAnnotation = null; + + for (AnnotationMirror annotationMirror : configRoot.getAnnotationMirrors()) { + String annotationName = utils.element().getQualifiedName(annotationMirror.getAnnotationType()); + + if (annotationName.equals(Types.ANNOTATION_CONFIG_ROOT)) { + configRootAnnotation = annotationMirror; + continue; + } + if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_PREFIX)) { + configDocPrefixAnnotation = annotationMirror; + continue; + } + if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_FILE_NAME)) { + configDocFileNameAnnotation = annotationMirror; + continue; + } + } + + if (configRootAnnotation == null) { + throw new IllegalStateException("@ConfigRoot is missing on " + configRoot); + } + + final Map elementValues = configRootAnnotation + .getElementValues(); + String name = Markers.HYPHENATED_ELEMENT_NAME; + for (Map.Entry entry : elementValues.entrySet()) { + final String key = entry.getKey().toString(); + final String value = entry.getValue().getValue().toString(); + if ("name()".equals(key)) { + name = value; + } else if ("phase()".equals(key)) { + configPhase = ConfigPhase.valueOf(value); + } else if ("prefix()".equals(key)) { + prefix = value; + } + } + + String overriddenDocPrefix = null; + if (configDocPrefixAnnotation != null) { + for (Map.Entry entry : configDocPrefixAnnotation + .getElementValues() + .entrySet()) { + if ("value()".equals(entry.getKey().toString())) { + overriddenDocPrefix = entry.getValue().getValue().toString(); + break; + } + } + } + + String overriddenDocFileName = null; + if (configDocFileNameAnnotation != null) { + for (Map.Entry entry : configDocFileNameAnnotation + .getElementValues() + .entrySet()) { + if ("value()".equals(entry.getKey().toString())) { + overriddenDocFileName = entry.getValue().getValue().toString(); + break; + } + } + } + + String rootPrefix = ConfigNamingUtil.getRootPrefix(prefix, name, configRoot.getSimpleName().toString(), configPhase); + String binaryName = utils.element().getBinaryName(configRoot); + + DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), + rootPrefix, overriddenDocPrefix, + binaryName, configRoot.getQualifiedName().toString(), + configPhase, overriddenDocFileName, false); + configCollector.addConfigRoot(discoveryConfigRoot); + return Optional.of(discoveryConfigRoot); + } + + @Override + public void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeElement clazz, VariableElement field, + ResolvedType resolvedType) { + if (config.getExtension().isMixedModule() && discoveryRootElement.isConfigMapping()) { + return; + } + + Map fieldAnnotations = utils.element().getAnnotations(field); + + String sourceName = field.getSimpleName().toString(); + String name = ConfigNamingUtil.hyphenate(sourceName); + + DiscoveryConfigProperty.Builder builder = DiscoveryConfigProperty.builder(clazz.getQualifiedName().toString(), + sourceName, SourceType.FIELD, resolvedType); + + AnnotationMirror configItemAnnotation = fieldAnnotations.get(Types.ANNOTATION_CONFIG_ITEM); + if (configItemAnnotation != null) { + Map configItemValues = utils.element().getAnnotationValues(configItemAnnotation); + + String configItemName = (String) configItemValues.get("name"); + if (configItemName != null && !Markers.HYPHENATED_ELEMENT_NAME.equals(configItemName)) { + name = configItemName; + } + + String configItemDefaultValue = (String) configItemValues.get("defaultValue"); + if (configItemDefaultValue != null && !Markers.NO_DEFAULT.equals(configItemDefaultValue)) { + builder.defaultValue(configItemDefaultValue); + } + + String configItemDefaultValueForDoc = (String) configItemValues.get("defaultValueDocumentation"); + if (!Strings.isEmpty(configItemDefaultValueForDoc)) { + builder.defaultValueForDoc(configItemDefaultValueForDoc); + } else { + // while ConfigDocDefault was added for ConfigMappings, it's allowed on fields so let's be safe + AnnotationMirror configDocDefaultAnnotation = fieldAnnotations.get(Types.ANNOTATION_CONFIG_DOC_DEFAULT); + if (configDocDefaultAnnotation != null) { + builder.defaultValueForDoc( + configDocDefaultAnnotation.getElementValues().values().iterator().next().getValue().toString()); + } + } + } + builder.name(name); + + if (resolvedType.isMap()) { + String mapKey = ConfigNamingUtil.hyphenate(sourceName); + AnnotationMirror configDocMapKeyAnnotation = fieldAnnotations.get(Types.ANNOTATION_CONFIG_DOC_MAP_KEY); + if (configDocMapKeyAnnotation != null) { + mapKey = configDocMapKeyAnnotation.getElementValues().values().iterator().next().getValue().toString(); + } + builder.mapKey(mapKey); + } + + if (fieldAnnotations.containsKey(Types.ANNOTATION_DEFAULT_CONVERTER) || + fieldAnnotations.containsKey(Types.ANNOTATION_CONVERT_WITH)) { + builder.converted(); + } + + handleCommonPropertyAnnotations(builder, fieldAnnotations, resolvedType, sourceName); + + discoveryRootElement.addProperty(builder.build()); + } + + @Deprecated(forRemoval = true) + @Override + public Optional onConfigGroup(TypeElement configGroup) { + if (config.getExtension().isMixedModule() && configGroup.getKind() == ElementKind.INTERFACE) { + return Optional.empty(); + } + + return super.onConfigGroup(configGroup); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/ConfigNamingUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/ConfigNamingUtil.java new file mode 100644 index 00000000000000..100e78e9898e14 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/ConfigNamingUtil.java @@ -0,0 +1,219 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import java.util.Iterator; +import java.util.Locale; +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.quarkus.annotation.processor.documentation.config.model.ConfigPhase; + +public final class ConfigNamingUtil { + + private static final String CONFIG = "Config"; + private static final String CONFIGURATION = "Configuration"; + private static final String HYPHEN = "-"; + private static final Pattern ENUM_SEPARATOR_PATTERN = Pattern.compile("([-_]+)"); + private static final String NAMED_MAP_CONFIG_ITEM_FORMAT = ".\"%s\""; + + private ConfigNamingUtil() { + } + + public static String getRootPrefix(String prefix, String name, String simpleClassName, ConfigPhase configPhase) { + String rootPrefix; + + if (name.equals(Markers.HYPHENATED_ELEMENT_NAME)) { + rootPrefix = deriveConfigRootName(simpleClassName, prefix, configPhase); + } else if (!prefix.isEmpty()) { + if (!name.isEmpty()) { + rootPrefix = prefix + Markers.DOT + name; + } else { + rootPrefix = prefix; + } + } else { + rootPrefix = name; + } + + if (rootPrefix.endsWith(Markers.DOT + Markers.PARENT)) { + // take into account the root case which would contain characters that can't be used to create the final file + rootPrefix = rootPrefix.replace(Markers.DOT + Markers.PARENT, ""); + } + + return rootPrefix; + } + + static String deriveConfigRootName(String simpleClassName, String prefix, ConfigPhase configPhase) { + String simpleNameInLowerCase = simpleClassName.toLowerCase(); + int length = simpleNameInLowerCase.length(); + + if (simpleNameInLowerCase.endsWith(CONFIG.toLowerCase())) { + String sanitized = simpleClassName.substring(0, length - CONFIG.length()); + return deriveConfigRootName(sanitized, prefix, configPhase); + } else if (simpleNameInLowerCase.endsWith(CONFIGURATION.toLowerCase())) { + String sanitized = simpleClassName.substring(0, length - CONFIGURATION.length()); + return deriveConfigRootName(sanitized, prefix, configPhase); + } else if (simpleNameInLowerCase.endsWith(configPhase.getConfigSuffix().toLowerCase())) { + String sanitized = simpleClassName.substring(0, length - configPhase.getConfigSuffix().length()); + return deriveConfigRootName(sanitized, prefix, configPhase); + } + + return !prefix.isEmpty() ? prefix + Markers.DOT + ConfigNamingUtil.hyphenate(simpleClassName) + : Markers.DEFAULT_PREFIX + Markers.DOT + ConfigNamingUtil.hyphenate(simpleClassName); + } + + public static Iterator camelHumpsIterator(String str) { + return new Iterator() { + int idx; + + @Override + public boolean hasNext() { + return idx < str.length(); + } + + @Override + public String next() { + if (idx == str.length()) + throw new NoSuchElementException(); + // known mixed-case rule-breakers + if (str.startsWith("JBoss", idx)) { + idx += 5; + return "JBoss"; + } + final int start = idx; + int c = str.codePointAt(idx); + if (Character.isUpperCase(c)) { + // an uppercase-starting word + idx = str.offsetByCodePoints(idx, 1); + if (idx < str.length()) { + c = str.codePointAt(idx); + if (Character.isUpperCase(c)) { + // all-caps word; need one look-ahead + int nextIdx = str.offsetByCodePoints(idx, 1); + while (nextIdx < str.length()) { + c = str.codePointAt(nextIdx); + if (Character.isLowerCase(c)) { + // ended at idx + return str.substring(start, idx); + } + idx = nextIdx; + nextIdx = str.offsetByCodePoints(idx, 1); + } + // consumed the whole remainder, update idx to length + idx = str.length(); + return str.substring(start); + } else { + // initial caps, trailing lowercase + idx = str.offsetByCodePoints(idx, 1); + while (idx < str.length()) { + c = str.codePointAt(idx); + if (Character.isUpperCase(c)) { + // end + return str.substring(start, idx); + } + idx = str.offsetByCodePoints(idx, 1); + } + // consumed the whole remainder + return str.substring(start); + } + } else { + // one-letter word + return str.substring(start); + } + } else { + // a lowercase-starting word + idx = str.offsetByCodePoints(idx, 1); + while (idx < str.length()) { + c = str.codePointAt(idx); + if (Character.isUpperCase(c)) { + // end + return str.substring(start, idx); + } + idx = str.offsetByCodePoints(idx, 1); + } + // consumed the whole remainder + return str.substring(start); + } + } + }; + } + + static Iterator lowerCase(Iterator orig) { + return new Iterator() { + @Override + public boolean hasNext() { + return orig.hasNext(); + } + + @Override + public String next() { + return orig.next().toLowerCase(Locale.ROOT); + } + }; + } + + static String join(Iterator it) { + final StringBuilder b = new StringBuilder(); + if (it.hasNext()) { + b.append(it.next()); + while (it.hasNext()) { + b.append("-"); + b.append(it.next()); + } + } + return b.toString(); + } + + public static String hyphenate(String orig) { + return join(lowerCase(camelHumpsIterator(orig))); + } + + /** + * This needs to be consistent with io.quarkus.runtime.configuration.HyphenateEnumConverter. + */ + public static String hyphenateEnumValue(String orig) { + StringBuffer target = new StringBuffer(); + String hyphenate = hyphenate(orig); + Matcher matcher = ENUM_SEPARATOR_PATTERN.matcher(hyphenate); + while (matcher.find()) { + matcher.appendReplacement(target, HYPHEN); + } + matcher.appendTail(target); + return target.toString(); + } + + static String normalizeDurationValue(String value) { + if (!value.isEmpty() && Character.isDigit(value.charAt(value.length() - 1))) { + try { + value = Integer.parseInt(value) + "S"; + } catch (NumberFormatException ignore) { + } + } + value = value.toUpperCase(Locale.ROOT); + return value; + } + + /** + * Replace each character that is neither alphanumeric nor _ with _ then convert the name to upper case, e.g. + * quarkus.datasource.jdbc.initial-size -> QUARKUS_DATASOURCE_JDBC_INITIAL_SIZE + * See also: io.smallrye.config.common.utils.StringUtil#replaceNonAlphanumericByUnderscores(java.lang.String) + */ + public static String toEnvVarName(final String name) { + int length = name.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = name.charAt(i); + if ('a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z' || + '0' <= c && c <= '9') { + sb.append(c); + } else { + sb.append('_'); + } + } + return sb.toString().toUpperCase(); + } + + public static String getMapKey(String mapKey) { + return String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, mapKey); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JacksonMappers.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JacksonMappers.java new file mode 100644 index 00000000000000..525da1c1788b62 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JacksonMappers.java @@ -0,0 +1,33 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +public final class JacksonMappers { + + private static final ObjectWriter JSON_OBJECT_WRITER = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_DEFAULT).writerWithDefaultPrettyPrinter(); + private static final ObjectWriter YAML_OBJECT_WRITER = new ObjectMapper(new YAMLFactory()) + .setSerializationInclusion(JsonInclude.Include.NON_DEFAULT).writer(); + private static final ObjectReader YAML_OBJECT_READER = new ObjectMapper(new YAMLFactory()) + .registerModule(new ParameterNamesModule()).reader(); + + private JacksonMappers() { + } + + public static ObjectWriter jsonObjectWriter() { + return JSON_OBJECT_WRITER; + } + + public static ObjectWriter yamlObjectWriter() { + return YAML_OBJECT_WRITER; + } + + public static ObjectReader yamlObjectReader() { + return YAML_OBJECT_READER; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java new file mode 100644 index 00000000000000..2bed8914e4cb00 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java @@ -0,0 +1,240 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jsoup.Jsoup; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; +import com.github.javaparser.javadoc.JavadocBlockTag.Type; +import com.github.javaparser.javadoc.description.JavadocDescription; + +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public final class JavadocUtil { + + private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); + private static final Pattern REPLACE_WINDOWS_EOL = Pattern.compile("\r\n"); + private static final Pattern REPLACE_MACOS_EOL = Pattern.compile("\r"); + private static final String DOT = "."; + private static final String NEW_LINE = "\n"; + + static final String VERTX_JAVA_DOC_SITE = "https://vertx.io/docs/apidocs/"; + static final String OFFICIAL_JAVA_DOC_BASE_LINK = "https://docs.oracle.com/en/java/javase/17/docs/api/java.base/"; + static final String AGROAL_API_JAVA_DOC_SITE = "https://javadoc.io/doc/io.agroal/agroal-api/latest/"; + static final String LOG_LEVEL_REDIRECT_URL = "https://javadoc.io/doc/org.jboss.logmanager/jboss-logmanager/latest/org/jboss/logmanager/Level.html"; + + private static final Pattern PACKAGE_PATTERN = Pattern.compile("^(\\w+)\\.(\\w+)\\..*$"); + + private static final Map EXTENSION_JAVA_DOC_LINK = new HashMap<>(); + + static { + EXTENSION_JAVA_DOC_LINK.put("io.vertx.", VERTX_JAVA_DOC_SITE); + EXTENSION_JAVA_DOC_LINK.put("io.agroal.", AGROAL_API_JAVA_DOC_SITE); + } + + private JavadocUtil() { + } + + public static ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { + if (rawJavadoc == null || rawJavadoc.isBlank()) { + return ParsedJavadoc.empty(); + } + + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + Javadoc javadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(rawJavadoc).replaceAll("* ")); + + String description; + JavadocFormat format; + + if (isAsciidoc(javadoc)) { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.ASCIIDOC; + } else if (isMarkdown(javadoc)) { + // this is to prepare the Markdown Javadoc that will come up soon enough + // I don't know exactly how the parser will deal with them though + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.MARKDOWN; + } else { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.JAVADOC; + } + + Optional since = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.SINCE) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + Optional deprecated = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.DEPRECATED) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + if (description != null && description.isBlank()) { + description = null; + } + + return new ParsedJavadoc(description, format, since.orElse(null), deprecated.orElse(null)); + } + + public static ParsedJavadocSection parseConfigSectionJavadoc(String javadocComment) { + if (javadocComment == null || javadocComment.trim().isEmpty()) { + return ParsedJavadocSection.empty(); + } + + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + javadocComment = START_OF_LINE.matcher(javadocComment).replaceAll("* "); + Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment); + + Optional deprecated = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.DEPRECATED) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + String description; + JavadocFormat format; + + if (isAsciidoc(javadoc)) { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.ASCIIDOC; + } else if (isMarkdown(javadoc)) { + // this is to prepare the Markdown Javadoc that will come up soon enough + // I don't know exactly how the parser will deal with them though + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.MARKDOWN; + } else { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.JAVADOC; + } + + if (description == null || description.isBlank()) { + return ParsedJavadocSection.empty(); + } + + final int newLineIndex = description.indexOf(NEW_LINE); + final int dotIndex = description.indexOf(DOT); + + final int endOfTitleIndex; + if (newLineIndex > 0 && newLineIndex < dotIndex) { + endOfTitleIndex = newLineIndex; + } else { + endOfTitleIndex = dotIndex; + } + + String title; + String details; + + if (endOfTitleIndex == -1) { + title = description.trim(); + details = null; + } else { + title = description.substring(0, endOfTitleIndex).trim(); + details = description.substring(endOfTitleIndex + 1).trim(); + } + + if (title.contains("<")) { + title = Jsoup.parse(title).text(); + } + + title = title.replaceAll("^([^\\w])+", ""); + + return new ParsedJavadocSection(title == null || title.isBlank() ? null : title, + details == null || details.isBlank() ? null : details, format, + deprecated.orElse(null)); + } + + /** + * Get javadoc link of a given type value + */ + public static String getJavadocSiteLink(String binaryName) { + if (binaryName.equals(Level.class.getName())) { + //hack, we don't want to link to the JUL version, but the jboss logging version + //this seems like a one off use case so for now it is just hacked in here + //if there are other use cases we should do something more generic + return LOG_LEVEL_REDIRECT_URL; + } + Matcher packageMatcher = PACKAGE_PATTERN.matcher(binaryName); + + if (!packageMatcher.find()) { + return null; + } + + if (TypeUtil.isPrimitiveWrapper(binaryName) || Types.ALIASED_TYPES.containsKey(binaryName)) { + return null; + } + + if ("java".equals(packageMatcher.group(1))) { + return OFFICIAL_JAVA_DOC_BASE_LINK + getJavaDocLinkForType(binaryName); + } + + String basePkgName = packageMatcher.group(1) + "." + packageMatcher.group(2) + "."; + String javaDocBaseUrl = EXTENSION_JAVA_DOC_LINK.get(basePkgName); + + if (javaDocBaseUrl != null) { + return javaDocBaseUrl + getJavaDocLinkForType(binaryName); + } + + return null; + } + + private static String normalizeEol(String javadoc) { + // it's Asciidoc, so we just pass through + // it also uses platform specific EOL, so we need to convert them back to \n + String normalizedJavadoc = javadoc; + normalizedJavadoc = REPLACE_WINDOWS_EOL.matcher(normalizedJavadoc).replaceAll("\n"); + normalizedJavadoc = REPLACE_MACOS_EOL.matcher(normalizedJavadoc).replaceAll("\n"); + return normalizedJavadoc; + } + + private static boolean isAsciidoc(Javadoc javadoc) { + for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { + if ("asciidoclet".equals(blockTag.getTagName())) { + return true; + } + } + return false; + } + + private static boolean isMarkdown(Javadoc javadoc) { + for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { + if ("markdown".equals(blockTag.getTagName())) { + return true; + } + } + return false; + } + + private static String getJavaDocLinkForType(String type) { + int beginOfWrappedTypeIndex = type.indexOf("<"); + if (beginOfWrappedTypeIndex != -1) { + type = type.substring(0, beginOfWrappedTypeIndex); + } + + int indexOfFirstUpperCase = 0; + for (int index = 0; index < type.length(); index++) { + char charAt = type.charAt(index); + if (charAt >= 'A' && charAt <= 'Z') { + indexOfFirstUpperCase = index; + break; + } + } + + final String base = type.substring(0, indexOfFirstUpperCase).replace('.', '/'); + final String html = type.substring(indexOfFirstUpperCase).replace('$', '.') + ".html"; + + return base + html; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Markers.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Markers.java new file mode 100644 index 00000000000000..a0ba6ac8854e4c --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Markers.java @@ -0,0 +1,15 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +public final class Markers { + + private Markers() { + } + + public static final String DEFAULT_PREFIX = "quarkus"; + public static final String PARENT = "<>"; + public static final String NO_DEFAULT = "<>"; + public static final String HYPHENATED_ELEMENT_NAME = "<>"; + public static final String DOT = "."; + public static final String DASH = "-"; + public static final String COMMA = ","; +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/TypeUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/TypeUtil.java new file mode 100644 index 00000000000000..01c5e094417311 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/TypeUtil.java @@ -0,0 +1,40 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import java.util.Locale; + +public final class TypeUtil { + + /* + * Retrieve a default value of a primitive type. + * + */ + public static String getPrimitiveDefaultValue(String primitiveType) { + return Types.PRIMITIVE_DEFAULT_VALUES.get(primitiveType); + } + + /** + * Replaces Java primitive wrapper types with primitive types + */ + public static String unbox(String type) { + String mapping = Types.PRIMITIVE_WRAPPERS.get(type); + return mapping == null ? type : mapping; + } + + public static boolean isPrimitiveWrapper(String type) { + return Types.PRIMITIVE_WRAPPERS.containsKey(type); + } + + public static String getAlias(String qualifiedName) { + return Types.ALIASED_TYPES.get(qualifiedName); + } + + public static String normalizeDurationValue(String value) { + if (!value.isEmpty() && Character.isDigit(value.charAt(value.length() - 1))) { + try { + value = Integer.parseInt(value) + "S"; + } catch (NumberFormatException ignore) { + } + } + return value.toUpperCase(Locale.ROOT); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Types.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Types.java new file mode 100644 index 00000000000000..bfb1e188f7ded3 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Types.java @@ -0,0 +1,77 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.Set; + +public final class Types { + + private Types() { + } + + public static final String ANNOTATION_RECORDER = "io.quarkus.runtime.annotations.Recorder"; + public static final String ANNOTATION_RECORD = "io.quarkus.deployment.annotations.Record"; + + public static final String MEMORY_SIZE_TYPE = "io.quarkus.runtime.configuration.MemorySize"; + public static final String ANNOTATION_CONFIG_ITEM = "io.quarkus.runtime.annotations.ConfigItem"; + public static final String ANNOTATION_BUILD_STEP = "io.quarkus.deployment.annotations.BuildStep"; + public static final String ANNOTATION_CONFIG_ROOT = "io.quarkus.runtime.annotations.ConfigRoot"; + public static final String ANNOTATION_CONFIG_MAPPING = "io.smallrye.config.ConfigMapping"; + public static final String ANNOTATION_DEFAULT_CONVERTER = "io.quarkus.runtime.annotations.DefaultConverter"; + public static final String ANNOTATION_CONVERT_WITH = "io.quarkus.runtime.annotations.ConvertWith"; + public static final String ANNOTATION_CONFIG_GROUP = "io.quarkus.runtime.annotations.ConfigGroup"; + public static final String ANNOTATION_CONFIG_DOC_IGNORE = "io.quarkus.runtime.annotations.ConfigDocIgnore"; + public static final String ANNOTATION_CONFIG_DOC_MAP_KEY = "io.quarkus.runtime.annotations.ConfigDocMapKey"; + public static final String ANNOTATION_CONFIG_DOC_SECTION = "io.quarkus.runtime.annotations.ConfigDocSection"; + public static final String ANNOTATION_CONFIG_DOC_ENUM_VALUE = "io.quarkus.runtime.annotations.ConfigDocEnumValue"; + public static final String ANNOTATION_CONFIG_DOC_DEFAULT = "io.quarkus.runtime.annotations.ConfigDocDefault"; + public static final String ANNOTATION_CONFIG_DOC_FILE_NAME = "io.quarkus.runtime.annotations.ConfigDocFilename"; + public static final String ANNOTATION_CONFIG_DOC_PREFIX = "io.quarkus.runtime.annotations.ConfigDocPrefix"; + public static final String ANNOTATION_CONFIG_DOC_ENUM = "io.quarkus.runtime.annotations.ConfigDocEnum"; + + public static final String ANNOTATION_CONFIG_WITH_CONVERTER = "io.smallrye.config.WithConverter"; + public static final String ANNOTATION_CONFIG_WITH_NAME = "io.smallrye.config.WithName"; + public static final String ANNOTATION_CONFIG_WITH_PARENT_NAME = "io.smallrye.config.WithParentName"; + public static final String ANNOTATION_CONFIG_WITH_DEFAULT = "io.smallrye.config.WithDefault"; + public static final String ANNOTATION_CONFIG_WITH_UNNAMED_KEY = "io.smallrye.config.WithUnnamedKey"; + + public static final Set SUPPORTED_ANNOTATIONS_TYPES = Set.of(ANNOTATION_BUILD_STEP, ANNOTATION_CONFIG_GROUP, + ANNOTATION_CONFIG_ROOT, ANNOTATION_RECORDER, ANNOTATION_CONFIG_MAPPING); + + static final Map ALIASED_TYPES = Map.of( + OptionalLong.class.getName(), long.class.getName(), + OptionalInt.class.getName(), int.class.getName(), + OptionalDouble.class.getName(), double.class.getName(), + "java.lang.Class", "class name", + "java.net.InetSocketAddress", "host:port", + Path.class.getName(), "path", + String.class.getName(), "string"); + + static final Map PRIMITIVE_DEFAULT_VALUES = new HashMap<>(); + + static final Map PRIMITIVE_WRAPPERS = new HashMap<>(); + + static { + PRIMITIVE_DEFAULT_VALUES.put("int", "0"); + PRIMITIVE_DEFAULT_VALUES.put("byte", "0"); + PRIMITIVE_DEFAULT_VALUES.put("char", ""); + PRIMITIVE_DEFAULT_VALUES.put("short", "0"); + PRIMITIVE_DEFAULT_VALUES.put("long", "0l"); + PRIMITIVE_DEFAULT_VALUES.put("float", "0f"); + PRIMITIVE_DEFAULT_VALUES.put("double", "0d"); + PRIMITIVE_DEFAULT_VALUES.put("boolean", "false"); + + PRIMITIVE_WRAPPERS.put("java.lang.Character", "char"); + PRIMITIVE_WRAPPERS.put("java.lang.Boolean", "boolean"); + PRIMITIVE_WRAPPERS.put("java.lang.Byte", "byte"); + PRIMITIVE_WRAPPERS.put("java.lang.Short", "short"); + PRIMITIVE_WRAPPERS.put("java.lang.Integer", "int"); + PRIMITIVE_WRAPPERS.put("java.lang.Long", "long"); + PRIMITIVE_WRAPPERS.put("java.lang.Float", "float"); + PRIMITIVE_WRAPPERS.put("java.lang.Double", "double"); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/extension/ExtensionBuildProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/extension/ExtensionBuildProcessor.java new file mode 100644 index 00000000000000..0c408d0bb494f8 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/extension/ExtensionBuildProcessor.java @@ -0,0 +1,191 @@ +package io.quarkus.annotation.processor.extension; + +import static javax.lang.model.util.ElementFilter.methodsIn; +import static javax.lang.model.util.ElementFilter.typesIn; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.tools.Diagnostic; + +import io.quarkus.annotation.processor.ExtensionProcessor; +import io.quarkus.annotation.processor.Outputs; +import io.quarkus.annotation.processor.documentation.config.util.Types; +import io.quarkus.annotation.processor.util.Config; +import io.quarkus.annotation.processor.util.Utils; + +public class ExtensionBuildProcessor implements ExtensionProcessor { + + private Config config; + private Utils utils; + + private final Set processorClassNames = new HashSet<>(); + private final Set recorderClassNames = new HashSet<>(); + private final Set configRootClassNames = new HashSet<>(); + private final Set buildSteps = new HashSet<>(); + private final Map annotationUsageTracker = new ConcurrentHashMap<>(); + + @Override + public void init(Config config, Utils utils) { + this.config = config; + this.utils = utils; + } + + @Override + public void process(Set annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + switch (annotation.getQualifiedName().toString()) { + case Types.ANNOTATION_BUILD_STEP: + trackAnnotationUsed(Types.ANNOTATION_BUILD_STEP); + processBuildStep(roundEnv, annotation); + break; + case Types.ANNOTATION_RECORDER: + trackAnnotationUsed(Types.ANNOTATION_RECORDER); + processRecorder(roundEnv, annotation); + break; + case Types.ANNOTATION_CONFIG_ROOT: + trackAnnotationUsed(Types.ANNOTATION_CONFIG_ROOT); + processConfigRoot(roundEnv, annotation); + break; + case Types.ANNOTATION_CONFIG_GROUP: + trackAnnotationUsed(Types.ANNOTATION_CONFIG_GROUP); + processConfigGroup(roundEnv, annotation); + break; + } + } + } + + @Override + public void finalizeProcessing() { + validateAnnotationUsage(); + + utils.filer().write(Outputs.META_INF_QUARKUS_BUILD_STEPS, buildSteps); + utils.filer().write(Outputs.META_INF_QUARKUS_CONFIG_ROOTS, configRootClassNames); + } + + private void processBuildStep(RoundEnvironment roundEnv, TypeElement annotation) { + for (ExecutableElement buildStep : methodsIn(roundEnv.getElementsAnnotatedWith(annotation))) { + final TypeElement clazz = utils.element().getClassOf(buildStep); + if (clazz == null) { + continue; + } + + final PackageElement pkg = utils.element().getPackageOf(clazz); + if (pkg == null) { + utils.processingEnv().getMessager().printMessage(Diagnostic.Kind.ERROR, + "Element " + clazz + " has no enclosing package"); + continue; + } + + final String binaryName = utils.element().getBinaryName(clazz); + if (processorClassNames.add(binaryName)) { + validateRecordBuildSteps(clazz); + utils.accessorGenerator().generateAccessor(clazz); + buildSteps.add(binaryName); + } + } + } + + private void validateRecordBuildSteps(TypeElement clazz) { + for (Element e : clazz.getEnclosedElements()) { + if (e.getKind() != ElementKind.METHOD) { + continue; + } + ExecutableElement ex = (ExecutableElement) e; + if (!utils.element().isAnnotationPresent(ex, Types.ANNOTATION_BUILD_STEP)) { + continue; + } + if (!utils.element().isAnnotationPresent(ex, Types.ANNOTATION_RECORD)) { + continue; + } + + boolean hasRecorder = false; + boolean allTypesResolvable = true; + for (VariableElement parameter : ex.getParameters()) { + String parameterClassName = parameter.asType().toString(); + TypeElement parameterTypeElement = utils.processingEnv().getElementUtils().getTypeElement(parameterClassName); + if (parameterTypeElement == null) { + allTypesResolvable = false; + } else { + if (utils.element().isAnnotationPresent(parameterTypeElement, Types.ANNOTATION_RECORDER)) { + if (parameterTypeElement.getModifiers().contains(Modifier.FINAL)) { + utils.processingEnv().getMessager().printMessage(Diagnostic.Kind.ERROR, + "Class '" + parameterTypeElement.getQualifiedName() + + "' is annotated with @Recorder and therefore cannot be made as a final class."); + } else if (utils.element().getPackageName(clazz) + .equals(utils.element().getPackageName(parameterTypeElement))) { + utils.processingEnv().getMessager().printMessage(Diagnostic.Kind.WARNING, + "Build step class '" + clazz.getQualifiedName() + + "' and recorder '" + parameterTypeElement + + "' share the same package. This is highly discouraged as it can lead to unexpected results."); + } + hasRecorder = true; + break; + } + } + } + + if (!hasRecorder && allTypesResolvable) { + utils.processingEnv().getMessager().printMessage(Diagnostic.Kind.ERROR, "Build Step '" + + clazz.getQualifiedName() + "#" + + ex.getSimpleName() + + "' which is annotated with '@Record' does not contain a method parameter whose type is annotated with '@Recorder'."); + } + } + } + + private void processRecorder(RoundEnvironment roundEnv, TypeElement annotation) { + for (TypeElement recorder : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { + if (recorderClassNames.add(recorder.getQualifiedName().toString())) { + utils.accessorGenerator().generateAccessor(recorder); + } + } + } + + private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation) { + for (TypeElement configRoot : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { + configRootClassNames.add(utils.element().getBinaryName(configRoot)); + + // TODO ideally we would use config.useConfigMapping() but core is currently a mess + // so using the annotations instead + if (!utils.element().isAnnotationPresent(configRoot, Types.ANNOTATION_CONFIG_MAPPING)) { + utils.accessorGenerator().generateAccessor(configRoot); + } + } + } + + private void processConfigGroup(RoundEnvironment roundEnv, TypeElement annotation) { + for (TypeElement configGroup : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { + // TODO for config groups, we generate an accessor only if we don't use @ConfigMapping + // and for core and messaging which are still a mess + if (!config.useConfigMapping() || config.getExtension().isMixedModule()) { + utils.accessorGenerator().generateAccessor(configGroup); + } + } + } + + private void validateAnnotationUsage() { + if (isAnnotationUsed(Types.ANNOTATION_BUILD_STEP) && isAnnotationUsed(Types.ANNOTATION_RECORDER)) { + utils.processingEnv().getMessager().printMessage(Diagnostic.Kind.ERROR, + "Detected use of @Recorder annotation in 'deployment' module. Classes annotated with @Recorder must be part of the extension's 'runtime' module"); + } + } + + private boolean isAnnotationUsed(String annotation) { + return annotationUsageTracker.getOrDefault(annotation, false); + } + + private void trackAnnotationUsed(String annotation) { + annotationUsageTracker.put(annotation, true); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoc.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoc.java deleted file mode 100644 index 0ad3e5a327d4d5..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoc.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.io.Writer; -import java.util.List; - -/** - * Represent one output file, its items are going to be appended to the file - */ -interface ConfigDoc { - - List getWriteItems(); - - /** - * An item is a summary table, note below the table, ... - */ - interface WriteItem { - void accept(Writer writer) throws IOException; - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocBuilder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocBuilder.java deleted file mode 100644 index 57edebb518529b..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocBuilder.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static io.quarkus.annotation.processor.Constants.SUMMARY_TABLE_ID_VARIABLE; -import static java.util.Objects.requireNonNull; - -import java.util.ArrayList; -import java.util.List; - -import io.quarkus.annotation.processor.Constants; - -/** - * {@link ConfigDoc} builder - */ -class ConfigDocBuilder { - - /** - * Declare AsciiDoc variable - */ - private static final String DECLARE_VAR = "\n:%s: %s\n"; - private final DocFormatter summaryTableDocFormatter; - protected final List writeItems = new ArrayList<>(); - - public ConfigDocBuilder() { - summaryTableDocFormatter = new SummaryTableDocFormatter(); - } - - protected ConfigDocBuilder(boolean showEnvVars) { - summaryTableDocFormatter = new SummaryTableDocFormatter(showEnvVars); - } - - /** - * Add documentation in a summary table and descriptive format - */ - public final ConfigDocBuilder addSummaryTable(String initialAnchorPrefix, boolean activateSearch, - List configDocItems, String fileName, - boolean includeConfigPhaseLegend) { - - writeItems.add(writer -> { - - // Create var with unique value for each summary table that will make DURATION_FORMAT_NOTE (see below) unique - var fileNameWithoutExtension = fileName.substring(0, fileName.length() - Constants.ADOC_EXTENSION.length()); - writer.append(String.format(DECLARE_VAR, SUMMARY_TABLE_ID_VARIABLE, fileNameWithoutExtension)); - - summaryTableDocFormatter.format(writer, initialAnchorPrefix, activateSearch, configDocItems, - includeConfigPhaseLegend); - - boolean hasDuration = false, hasMemory = false; - for (ConfigDocItem item : configDocItems) { - if (item.hasDurationInformationNote()) { - hasDuration = true; - } - - if (item.hasMemoryInformationNote()) { - hasMemory = true; - } - } - - if (hasDuration) { - writer.append(Constants.DURATION_FORMAT_NOTE); - } - - if (hasMemory) { - writer.append(Constants.MEMORY_SIZE_FORMAT_NOTE); - } - }); - return this; - } - - public boolean hasWriteItems() { - return !writeItems.isEmpty(); - } - - /** - * Passed strings are appended to the file - */ - public final ConfigDocBuilder write(String... strings) { - requireNonNull(strings); - writeItems.add(writer -> { - for (String str : strings) { - writer.append(str); - } - }); - return this; - } - - public final ConfigDoc build() { - final List docItemsCopy = List.copyOf(writeItems); - return () -> docItemsCopy; - } - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocElement.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocElement.java deleted file mode 100644 index e23d313841fbe3..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocElement.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static io.quarkus.annotation.processor.generate_doc.ConfigPhase.COMPARATOR; - -import java.io.IOException; -import java.io.Writer; - -public interface ConfigDocElement { - void accept(Writer writer, DocFormatter docFormatter) throws IOException; - - ConfigPhase getConfigPhase(); - - boolean isWithinAMap(); - - String getTopLevelGrouping(); - - /** - * - * Map config will be at the end of generated doc. - * Order build time config first - * Otherwise maintain source code order. - */ - default int compare(ConfigDocElement item) { - if (isWithinAMap()) { - if (item.isWithinAMap()) { - return COMPARATOR.compare(getConfigPhase(), item.getConfigPhase()); - } - return 1; - } else if (item.isWithinAMap()) { - return -1; - } - - return COMPARATOR.compare(getConfigPhase(), item.getConfigPhase()); - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocGeneratedOutput.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocGeneratedOutput.java deleted file mode 100644 index 95715a245e8975..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocGeneratedOutput.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.util.List; -import java.util.Objects; - -import io.quarkus.annotation.processor.Constants; - -public class ConfigDocGeneratedOutput { - private final String fileName; - private final boolean searchable; - private final boolean hasAnchorPrefix; - private final List configDocItems; - - public ConfigDocGeneratedOutput(String fileName, boolean searchable, List configDocItems, - boolean hasAnchorPrefix) { - this.fileName = fileName; - this.searchable = searchable; - this.configDocItems = configDocItems; - this.hasAnchorPrefix = hasAnchorPrefix; - } - - public String getFileName() { - return fileName; - } - - public boolean isSearchable() { - return searchable; - } - - public List getConfigDocItems() { - return configDocItems; - } - - public String getAnchorPrefix() { - if (!hasAnchorPrefix) { - return Constants.EMPTY; - } - - String anchorPrefix = fileName; - if (fileName.endsWith(Constants.ADOC_EXTENSION)) { - anchorPrefix = anchorPrefix.substring(0, anchorPrefix.length() - 5); - } - - return anchorPrefix + "_"; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - ConfigDocGeneratedOutput that = (ConfigDocGeneratedOutput) o; - return Objects.equals(fileName, that.fileName); - } - - @Override - public int hashCode() { - return Objects.hash(fileName); - } - - @Override - public String toString() { - return "ConfigItemsOutput{" + - "fileName='" + fileName + '\'' + - ", searchable=" + searchable + - ", configDocItems=" + configDocItems + - '}'; - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItem.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItem.java deleted file mode 100644 index 3055ab5165bd7c..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItem.java +++ /dev/null @@ -1,170 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.io.Writer; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * A config doc item is either a config section {@link ConfigDocSection} or a config key {@link ConfigDocKey} - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -final public class ConfigDocItem implements ConfigDocElement, Comparable { - private ConfigDocKey configDocKey; - private ConfigDocSection configDocSection; - - public ConfigDocItem() { - } - - public ConfigDocItem(ConfigDocSection configDocSection, ConfigDocKey configDocKey) { - this.configDocSection = configDocSection; - this.configDocKey = configDocKey; - } - - public ConfigDocKey getConfigDocKey() { - return configDocKey; - } - - public void setConfigDocKey(ConfigDocKey configDocKey) { - this.configDocKey = configDocKey; - } - - public ConfigDocSection getConfigDocSection() { - return configDocSection; - } - - public void setConfigDocSection(ConfigDocSection configDocSection) { - this.configDocSection = configDocSection; - } - - @JsonIgnore - boolean isConfigSection() { - return configDocSection != null; - } - - @JsonIgnore - boolean isConfigKey() { - return configDocKey != null; - } - - @Override - public String toString() { - return "ConfigDocItem{" + - "configDocSection=" + configDocSection + - ", configDocKey=" + configDocKey + - '}'; - } - - @Override - public void accept(Writer writer, DocFormatter docFormatter) throws IOException { - if (isConfigSection()) { - configDocSection.accept(writer, docFormatter); - } else if (isConfigKey()) { - configDocKey.accept(writer, docFormatter); - } - } - - @JsonIgnore - @Override - public ConfigPhase getConfigPhase() { - if (isConfigSection()) { - return configDocSection.getConfigPhase(); - } else if (isConfigKey()) { - return configDocKey.getConfigPhase(); - } - - return null; - } - - @JsonIgnore - @Override - public boolean isWithinAMap() { - if (isConfigSection()) { - return configDocSection.isWithinAMap(); - } else if (isConfigKey()) { - return configDocKey.isWithinAMap(); - } - - return false; - } - - @Override - @JsonIgnore - public String getTopLevelGrouping() { - if (isConfigKey()) { - return configDocKey.getTopLevelGrouping(); - } else if (isConfigSection()) { - return configDocSection.getTopLevelGrouping(); - } - - return null; - } - - @JsonIgnore - public boolean isWithinAConfigGroup() { - if (isConfigSection()) { - return true; - } else if (isConfigKey() && configDocKey.isWithinAConfigGroup()) { - return true; - } - - return false; - } - - /** - * TODO determine section ordering - * - * @param item - * @return - */ - @Override - public int compareTo(ConfigDocItem item) { - // ensure that different config objects in the same extension don't cross streams - if (isConfigKey() && item.isConfigKey() && (!getTopLevelGrouping().equals(item.getTopLevelGrouping()))) { - return getTopLevelGrouping().compareTo(item.getTopLevelGrouping()); - } - - if (isConfigSection() && item.isConfigKey()) { - return 1; // push sections to the end of the list - } else if (isConfigKey() && item.isConfigSection()) { - return -1; // push section to the end of the list - } - - return compare(item); - } - - public boolean hasDurationInformationNote() { - if (isConfigKey()) { - return DocGeneratorUtil.hasDurationInformationNote(configDocKey); - } else if (isConfigSection()) { - return configDocSection.hasDurationInformationNote(); - } - return false; - } - - public boolean hasMemoryInformationNote() { - if (isConfigKey()) { - return DocGeneratorUtil.hasMemoryInformationNote(configDocKey); - } else if (isConfigSection()) { - return configDocSection.hasMemoryInformationNote(); - } - return false; - } - - public void configPhase(ConfigPhase phase) { - if (isConfigKey()) { - configDocKey.setConfigPhase(phase); - } else { - configDocSection.setConfigPhase(phase); - } - } - - public void withinAMap(boolean withinAMap) { - if (isConfigKey()) { - configDocKey.setWithinAMap(configDocKey.isWithinAMap() || withinAMap); - } else { - configDocSection.setWithinAMap(configDocSection.isWithinAMap() || withinAMap); - } - } -} 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 deleted file mode 100644 index a79513b5c46bd8..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java +++ /dev/null @@ -1,674 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -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; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_WITH_DEFAULT; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_WITH_NAME; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_WITH_PARENT_NAME; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONFIG_WITH_UNNAMED_KEY; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_CONVERT_WITH; -import static io.quarkus.annotation.processor.Constants.ANNOTATION_DEFAULT_CONVERTER; -import static io.quarkus.annotation.processor.Constants.DOT; -import static io.quarkus.annotation.processor.Constants.EMPTY; -import static io.quarkus.annotation.processor.Constants.HYPHENATED_ELEMENT_NAME; -import static io.quarkus.annotation.processor.Constants.LIST_OF_CONFIG_ITEMS_TYPE_REF; -import static io.quarkus.annotation.processor.Constants.NEW_LINE; -import static io.quarkus.annotation.processor.Constants.NO_DEFAULT; -import static io.quarkus.annotation.processor.Constants.OBJECT_MAPPER; -import static io.quarkus.annotation.processor.Constants.PARENT; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.getJavaDocSiteLink; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.getKnownGenericType; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.hyphenate; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.hyphenateEnumValue; -import static java.util.Collections.emptyList; -import static java.util.stream.Collectors.toList; -import static javax.lang.model.element.Modifier.ABSTRACT; - -import java.io.IOException; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.Name; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.ArrayType; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.ExecutableType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import io.quarkus.annotation.processor.Constants; -import io.quarkus.annotation.processor.generate_doc.JavaDocParser.SectionHolder; - -class ConfigDocItemFinder { - - private static final String COMMA = ","; - private static final String BACK_TICK = "`"; - private static final String NAMED_MAP_CONFIG_ITEM_FORMAT = ".\"%s\""; - private static final Set PRIMITIVE_TYPES = new HashSet<>( - Arrays.asList("byte", "short", "int", "long", "float", "double", "boolean", "char")); - - private final JavaDocParser javaDocParser = new JavaDocParser(); - private final JavaDocParser enumJavaDocParser = new JavaDocParser(true); - private final ScannedConfigDocsItemHolder holder = new ScannedConfigDocsItemHolder(); - - private final Set configRoots; - private final Properties javaDocProperties; - private final Map configGroupQualifiedNameToTypeElementMap; - private final FsMap allConfigurationGroups; - private final FsMap allConfigurationRoots; - private final boolean configMapping; - - public ConfigDocItemFinder(Set configRoots, - Map configGroupQualifiedNameToTypeElementMap, - Properties javaDocProperties, FsMap allConfigurationGroups, FsMap allConfigurationRoots, - boolean configMapping) { - this.configRoots = configRoots; - this.configGroupQualifiedNameToTypeElementMap = configGroupQualifiedNameToTypeElementMap; - this.javaDocProperties = javaDocProperties; - this.allConfigurationGroups = allConfigurationGroups; - this.allConfigurationRoots = allConfigurationRoots; - this.configMapping = configMapping; - } - - /** - * Find configuration items from current encountered configuration roots. - * Scan configuration group first and record them in a properties file as they can be shared across - * different modules. - * - */ - ScannedConfigDocsItemHolder findInMemoryConfigurationItems() throws IOException { - for (Map.Entry entry : configGroupQualifiedNameToTypeElementMap.entrySet()) { - ConfigPhase buildTime = ConfigPhase.BUILD_TIME; - final List configDocItems = recursivelyFindConfigItems(entry.getValue(), EMPTY, EMPTY, buildTime, - false, 1, - false, configMapping); - allConfigurationGroups.put(entry.getKey(), OBJECT_MAPPER.writeValueAsString(configDocItems)); - } - - for (ConfigRootInfo configRootInfo : configRoots) { - final int sectionLevel = 0; - final TypeElement element = configRootInfo.getClazz(); - String rootName = configRootInfo.getName(); - ConfigPhase configPhase = configRootInfo.getConfigPhase(); - final List configDocItems = recursivelyFindConfigItems(element, rootName, rootName, configPhase, - false, sectionLevel, true, configMapping); - holder.addConfigRootItems(configRootInfo, configDocItems); - allConfigurationRoots.put(configRootInfo.getClazz().toString(), OBJECT_MAPPER.writeValueAsString(configDocItems)); - } - - return holder; - } - - /** - * Recursively find config item found in a config root or config group given as {@link Element} - */ - private List recursivelyFindConfigItems(Element element, String rootName, String parentName, - ConfigPhase configPhase, boolean withinAMap, int sectionLevel, boolean generateSeparateConfigGroupDocsFiles, - boolean configMapping) - throws JsonProcessingException { - List configDocItems = new ArrayList<>(); - TypeElement asTypeElement = (TypeElement) element; - List superTypes = new ArrayList<>(); - superTypes.add(asTypeElement.getSuperclass()); - superTypes.addAll(asTypeElement.getInterfaces()); - - for (TypeMirror superType : superTypes) { - if (superType.getKind() != TypeKind.NONE && !superType.toString().equals(Object.class.getName())) { - String key = superType.toString(); - String rawConfigItems = allConfigurationGroups.get(key); - if (rawConfigItems == null) { - rawConfigItems = allConfigurationRoots.get(key); - } - final List superTypeConfigItems; - if (rawConfigItems == null) { // element not yet scanned - Element superElement = ((DeclaredType) superType).asElement(); - superTypeConfigItems = recursivelyFindConfigItems(superElement, rootName, parentName, - configPhase, withinAMap, sectionLevel, generateSeparateConfigGroupDocsFiles, - configMapping); - } else { - superTypeConfigItems = OBJECT_MAPPER.readValue(rawConfigItems, LIST_OF_CONFIG_ITEMS_TYPE_REF); - } - - configDocItems.addAll(superTypeConfigItems); - } - } - - for (Element enclosedElement : element.getEnclosedElements()) { - if (!shouldProcessElement(enclosedElement, configMapping)) { - continue; - } - - boolean isStaticField = enclosedElement - .getModifiers() - .stream() - .anyMatch(Modifier.STATIC::equals); - - if (isStaticField) { - continue; - } - - String name = null; - String defaultValue = NO_DEFAULT; - String defaultValueDoc = EMPTY; - List acceptedValues = null; - final TypeElement clazz = (TypeElement) element; - final String fieldName = enclosedElement.getSimpleName().toString(); - final String javaDocKey = clazz.getQualifiedName().toString() + DOT + fieldName; - final List annotationMirrors = enclosedElement.getAnnotationMirrors(); - final String rawJavaDoc = javaDocProperties.getProperty(javaDocKey); - boolean useHyphenateEnumValue = true; - - String hyphenatedFieldName = hyphenate(fieldName); - String configDocMapKey = hyphenatedFieldName; - boolean unnamedMapKey = false; - boolean isDeprecated = false; - boolean generateDocumentation = true; - ConfigDocSection configSection = new ConfigDocSection(); - configSection.setTopLevelGrouping(rootName); - configSection.setWithinAMap(withinAMap); - configSection.setConfigPhase(configPhase); - - for (AnnotationMirror annotationMirror : annotationMirrors) { - String annotationName = annotationMirror.getAnnotationType().toString(); - if (annotationName.equals(Deprecated.class.getName())) { - isDeprecated = true; - break; - } - if (annotationName.equals(ANNOTATION_CONFIG_ITEM) - || annotationName.equals(ANNOTATION_CONFIG_DOC_MAP_KEY)) { - for (Map.Entry entry : annotationMirror - .getElementValues().entrySet()) { - final String key = entry.getKey().toString(); - final Object value = entry.getValue().getValue(); - if (annotationName.equals(ANNOTATION_CONFIG_DOC_MAP_KEY) && "value()".equals(key)) { - configDocMapKey = value.toString(); - } else if (annotationName.equals(ANNOTATION_CONFIG_ITEM)) { - if ("name()".equals(key)) { - switch (value.toString()) { - case HYPHENATED_ELEMENT_NAME: - name = parentName + DOT + hyphenatedFieldName; - break; - case PARENT: - name = parentName; - break; - default: - name = parentName + DOT + value; - } - } else if ("defaultValue()".equals(key)) { - defaultValue = value.toString(); - } else if ("defaultValueDocumentation()".equals(key)) { - defaultValueDoc = value.toString(); - } else if ("generateDocumentation()".equals(key)) { - generateDocumentation = (Boolean) value; - } - } - } - } else if (annotationName.equals(ANNOTATION_CONFIG_DOC_SECTION)) { - final SectionHolder sectionHolder = javaDocParser.parseConfigSection(rawJavaDoc, sectionLevel); - configSection.setShowSection(true); - configSection.setSectionDetails(sectionHolder.details); - configSection.setSectionDetailsTitle(sectionHolder.title); - configSection.setName(parentName + DOT + hyphenatedFieldName); - } else if (annotationName.equals(ANNOTATION_DEFAULT_CONVERTER) - || annotationName.equals(ANNOTATION_CONVERT_WITH)) { - useHyphenateEnumValue = false; - } - - // Mappings - if (annotationName.equals(ANNOTATION_CONFIG_WITH_NAME)) { - name = parentName + DOT + annotationMirror.getElementValues().values().iterator().next().getValue(); - } else if (annotationName.equals(ANNOTATION_CONFIG_WITH_PARENT_NAME)) { - name = parentName; - } else if (annotationName.equals(ANNOTATION_CONFIG_DOC_DEFAULT)) { - defaultValueDoc = annotationMirror.getElementValues().values().iterator().next().getValue().toString(); - } else if (annotationName.equals(ANNOTATION_CONFIG_WITH_DEFAULT)) { - defaultValue = annotationMirror.getElementValues().values().isEmpty() ? null - : 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; - } - } - - if (isDeprecated) { - continue; // do not include deprecated config items - } - if (!generateDocumentation) { - continue; // documentation for this item was explicitly disabled - } - - if (name == null) { - name = parentName + DOT + hyphenatedFieldName; - } - if (NO_DEFAULT.equals(defaultValue)) { - defaultValue = EMPTY; - } - - TypeMirror typeMirror = unwrapTypeMirror(enclosedElement.asType()); - String type = getType(typeMirror); - - if (isConfigGroup(type)) { - List groupConfigItems = readConfigGroupItems(configPhase, rootName, name, emptyList(), type, - configSection, withinAMap, generateSeparateConfigGroupDocsFiles, configMapping); - DocGeneratorUtil.appendConfigItemsIntoExistingOnes(configDocItems, groupConfigItems); - } else { - final ConfigDocKey configDocKey = new ConfigDocKey(); - configDocKey.setWithinAMap(withinAMap); - boolean list = false; - boolean optional = false; - if (!typeMirror.getKind().isPrimitive()) { - DeclaredType declaredType = (DeclaredType) typeMirror; - TypeElement typeElement = (TypeElement) declaredType.asElement(); - Name qualifiedName = typeElement.getQualifiedName(); - optional = qualifiedName.toString().startsWith(Optional.class.getName()) - || qualifiedName.contentEquals(Map.class.getName()); - list = qualifiedName.contentEquals(List.class.getName()) - || qualifiedName.contentEquals(Set.class.getName()); - - List typeArguments = declaredType.getTypeArguments(); - if (!typeArguments.isEmpty()) { - // FIXME: this is super dodgy: we should check the type!! - if (typeArguments.size() == 2) { - type = getType(typeArguments.get(1)); - List additionalNames; - if (unnamedMapKey) { - additionalNames = List - .of(name + String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, configDocMapKey)); - } else { - name += String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, configDocMapKey); - additionalNames = emptyList(); - } - if (isConfigGroup(type)) { - List groupConfigItems = readConfigGroupItems(configPhase, rootName, name, - additionalNames, type, configSection, true, generateSeparateConfigGroupDocsFiles, - configMapping); - DocGeneratorUtil.appendConfigItemsIntoExistingOnes(configDocItems, groupConfigItems); - continue; - } else { - configDocKey.setWithinAMap(true); - } - } else { - // FIXME: this is for Optional and List - TypeMirror realTypeMirror = typeArguments.get(0); - String typeInString = realTypeMirror.toString(); - - if (optional) { - if (isConfigGroup(typeInString)) { - if (!configSection.isShowSection()) { - final SectionHolder sectionHolder = javaDocParser.parseConfigSection( - rawJavaDoc, - sectionLevel); - configSection.setSectionDetails(sectionHolder.details); - configSection.setSectionDetailsTitle(sectionHolder.title); - configSection.setName(parentName + DOT + hyphenatedFieldName); - configSection.setShowSection(true); - } - configSection.setOptional(true); - List groupConfigItems = readConfigGroupItems(configPhase, rootName, name, - emptyList(), typeInString, configSection, withinAMap, - generateSeparateConfigGroupDocsFiles, configMapping); - DocGeneratorUtil.appendConfigItemsIntoExistingOnes(configDocItems, groupConfigItems); - continue; - } else if ((typeInString.startsWith(List.class.getName()) - || typeInString.startsWith(Set.class.getName()) - || realTypeMirror.getKind() == TypeKind.ARRAY)) { - list = true; - DeclaredType declaredRealType = (DeclaredType) typeMirror; - typeArguments = declaredRealType.getTypeArguments(); - if (!typeArguments.isEmpty()) { - realTypeMirror = typeArguments.get(0); - } - } - } - - type = simpleTypeToString(realTypeMirror); - if (isEnumType(realTypeMirror)) { - if (defaultValueDoc.isBlank()) { - if (useHyphenateEnumValue) { - defaultValue = Arrays.stream(defaultValue.split(COMMA)) - .map(defaultEnumValue -> hyphenateEnumValue(defaultEnumValue.trim())) - .collect(Collectors.joining(COMMA)); - } - } else { - defaultValue = defaultValueDoc; - } - acceptedValues = extractEnumValues(realTypeMirror, useHyphenateEnumValue, - clazz.getQualifiedName().toString()); - configDocKey.setEnum(true); - } else { - if (!defaultValueDoc.isBlank()) { - defaultValue = defaultValueDoc; - } - } - } - } else { - type = simpleTypeToString(declaredType); - - if (defaultValueDoc.isBlank()) { - if (isEnumType(declaredType)) { - defaultValue = hyphenateEnumValue(defaultValue); - acceptedValues = extractEnumValues(declaredType, useHyphenateEnumValue, - clazz.getQualifiedName().toString()); - configDocKey.setEnum(true); - } else if (isDurationType(declaredType) && !defaultValue.isEmpty()) { - defaultValue = DocGeneratorUtil.normalizeDurationValue(defaultValue); - } - } else { - defaultValue = defaultValueDoc; - } - } - } - - configDocKey.setKey(name); - configDocKey.setAdditionalKeys(emptyList()); - configDocKey.setType(type); - configDocKey.setList(list); - configDocKey.setOptional(optional); - configDocKey.setWithinAConfigGroup(sectionLevel > 0); - configDocKey.setTopLevelGrouping(rootName); - configDocKey.setConfigPhase(configPhase); - configDocKey.setDefaultValue(defaultValue); - configDocKey.setDocMapKey(configDocMapKey); - javaDocParser.parseConfigDescription(rawJavaDoc, configDocKey::setConfigDoc, configDocKey::setSince); - configDocKey.setEnvironmentVariable(DocGeneratorUtil.toEnvVarName(name)); - configDocKey.setAcceptedValues(acceptedValues); - configDocKey.setJavaDocSiteLink(getJavaDocSiteLink(type)); - ConfigDocItem configDocItem = new ConfigDocItem(); - configDocItem.setConfigDocKey(configDocKey); - configDocItems.add(configDocItem); - } - } - - return configDocItems; - } - - private TypeMirror unwrapTypeMirror(TypeMirror typeMirror) { - if (typeMirror instanceof DeclaredType) { - return typeMirror; - } - - if (typeMirror instanceof ExecutableType) { - ExecutableType executableType = (ExecutableType) typeMirror; - return executableType.getReturnType(); - } - - return typeMirror; - } - - private String getType(TypeMirror typeMirror) { - if (typeMirror instanceof DeclaredType) { - DeclaredType declaredType = (DeclaredType) typeMirror; - TypeElement typeElement = (TypeElement) declaredType.asElement(); - return typeElement.getQualifiedName().toString(); - } - return typeMirror.toString(); - } - - private boolean isConfigGroup(String type) { - if (type.startsWith("java.") || PRIMITIVE_TYPES.contains(type)) { - return false; - } - return configGroupQualifiedNameToTypeElementMap.containsKey(type) || allConfigurationGroups.hasKey(type); - } - - private boolean shouldProcessElement(final Element enclosedElement, final boolean configMapping) { - if (enclosedElement.getKind().isField()) { - return true; - } - - if (!configMapping && enclosedElement.getKind() == ElementKind.METHOD) { - return false; - } - - // A ConfigMapping method - if (enclosedElement.getKind().equals(ElementKind.METHOD)) { - ExecutableElement method = (ExecutableElement) enclosedElement; - // Skip toString method, because mappings can include it and generate it - if (method.getSimpleName().contentEquals("toString") && method.getParameters().size() == 0) { - return false; - } - Element enclosingElement = enclosedElement.getEnclosingElement(); - return enclosingElement.getModifiers().contains(ABSTRACT) && enclosedElement.getModifiers().contains(ABSTRACT); - } - - return false; - } - - private String simpleTypeToString(TypeMirror typeMirror) { - if (typeMirror.getKind().isPrimitive()) { - return typeMirror.toString(); - } else if (typeMirror.getKind() == TypeKind.ARRAY) { - return simpleTypeToString(((ArrayType) typeMirror).getComponentType()); - } - - final String knownGenericType = getKnownGenericType((DeclaredType) typeMirror); - - if (knownGenericType != null) { - return knownGenericType; - } - - List typeArguments = ((DeclaredType) typeMirror).getTypeArguments(); - if (!typeArguments.isEmpty()) { - return simpleTypeToString(typeArguments.get(0)); - } - - return getType(typeMirror); - } - - private List extractEnumValues(TypeMirror realTypeMirror, boolean useHyphenatedEnumValue, String javaDocKey) { - Element declaredTypeElement = ((DeclaredType) realTypeMirror).asElement(); - List acceptedValues = new ArrayList<>(); - - for (Element field : declaredTypeElement.getEnclosedElements()) { - if (field.getKind() == ElementKind.ENUM_CONSTANT) { - String enumValue = field.getSimpleName().toString(); - - // Find enum constant description - final String constantJavaDocKey = javaDocKey + DOT + enumValue; - final String rawJavaDoc = javaDocProperties.getProperty(constantJavaDocKey); - - String explicitEnumValueName = extractEnumValueName(field); - if (explicitEnumValueName != null) { - enumValue = explicitEnumValueName; - } else { - enumValue = useHyphenatedEnumValue ? hyphenateEnumValue(enumValue) : enumValue; - } - if (rawJavaDoc != null && !rawJavaDoc.isBlank()) { - // Show enum constant description as a Tooltip - String javaDoc = enumJavaDocParser.parseConfigDescription(rawJavaDoc); - acceptedValues.add(String.format(Constants.TOOLTIP, enumValue, - javaDoc.replace("

", EMPTY).replace("

", EMPTY).replace(NEW_LINE, " "))); - } else { - acceptedValues.add(Constants.CODE_DELIMITER - + enumValue + Constants.CODE_DELIMITER); - } - } - } - - return acceptedValues; - } - - private String extractEnumValueName(Element enumField) { - for (AnnotationMirror annotationMirror : enumField.getAnnotationMirrors()) { - String annotationName = annotationMirror.getAnnotationType().toString(); - if (annotationName.equals(ANNOTATION_CONFIG_DOC_ENUM_VALUE)) { - for (var entry : annotationMirror.getElementValues().entrySet()) { - var key = entry.getKey().toString(); - var value = entry.getValue().getValue(); - if ("value()".equals(key)) { - return value.toString(); - } - } - } - } - return null; - } - - private boolean isEnumType(TypeMirror realTypeMirror) { - return realTypeMirror instanceof DeclaredType - && ((DeclaredType) realTypeMirror).asElement().getKind() == ElementKind.ENUM; - } - - private boolean isDurationType(TypeMirror realTypeMirror) { - return realTypeMirror.toString().equals(Duration.class.getName()); - } - - /** - * Scan or parse configuration items of a given configuration group. - *

- * If the configuration group is already scanned, retrieve the scanned items and parse them - * If not, make sure that items of a given configuration group are properly scanned and the record of scanned - * configuration group if properly updated afterwards. - * - */ - private List readConfigGroupItems( - ConfigPhase configPhase, - String topLevelRootName, - String parentName, - List additionalNames, - String configGroup, - ConfigDocSection configSection, - boolean withinAMap, - boolean generateSeparateConfigGroupDocs, - boolean configMapping) - throws JsonProcessingException { - - configSection.setConfigGroupType(configGroup); - if (configSection.getSectionDetailsTitle() == null) { - configSection.setSectionDetailsTitle(parentName); - } - - if (configSection.getName() == null) { - configSection.setName(EMPTY); - } - - final List configDocItems = new ArrayList<>(); - String property = allConfigurationGroups.get(configGroup); - List groupConfigItems; - if (property != null) { - groupConfigItems = OBJECT_MAPPER.readValue(property, LIST_OF_CONFIG_ITEMS_TYPE_REF); - } else { - TypeElement configGroupTypeElement = configGroupQualifiedNameToTypeElementMap.get(configGroup); - groupConfigItems = recursivelyFindConfigItems(configGroupTypeElement, EMPTY, EMPTY, configPhase, - false, 1, generateSeparateConfigGroupDocs, configMapping); - allConfigurationGroups.put(configGroup, OBJECT_MAPPER.writeValueAsString(groupConfigItems)); - } - - groupConfigItems = decorateGroupItems(groupConfigItems, configPhase, topLevelRootName, parentName, additionalNames, - withinAMap, generateSeparateConfigGroupDocs); - - // make sure that the config section is added if it is to be shown or when scanning parent configuration group - // priory to scanning configuration roots. This is useful as we get indication of whether the config items are part - // of a configuration section (i.e. configuration group) we are current scanning. - if (configSection.isShowSection() || !generateSeparateConfigGroupDocs) { - final ConfigDocItem configDocItem = new ConfigDocItem(); - configDocItem.setConfigDocSection(configSection); - configDocItems.add(configDocItem); - configSection.addConfigDocItems(groupConfigItems); - } else { - configDocItems.addAll(groupConfigItems); - } - - if (generateSeparateConfigGroupDocs) { - addConfigGroupItemToHolder(configDocItems, configGroup); - } - - return configDocItems; - } - - /** - * Add some information which are missing from configuration items scanned from configuration groups. - * The missing information come from configuration roots and these are config phase, top level root name and parent name (as - * we are traversing down the tree) - */ - private List decorateGroupItems( - List groupConfigItems, - ConfigPhase configPhase, - String topLevelRootName, - String parentName, - List additionalNames, - boolean withinAMap, - boolean generateSeparateConfigGroupDocs) { - - List decoratedItems = new ArrayList<>(); - for (ConfigDocItem configDocItem : groupConfigItems) { - if (configDocItem.isConfigKey()) { - ConfigDocKey configDocKey = configDocItem.getConfigDocKey(); - configDocKey.setConfigPhase(configPhase); - configDocKey.setWithinAMap(configDocKey.isWithinAMap() || withinAMap); - configDocKey.setWithinAConfigGroup(true); - configDocKey.setTopLevelGrouping(topLevelRootName); - List additionalKeys = new ArrayList<>(); - for (String key : configDocKey.getAdditionalKeys()) { - additionalKeys.add(parentName + key); - for (String name : additionalNames) { - additionalKeys.add(name + key); - } - } - 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(); - section.setConfigPhase(configPhase); - section.setTopLevelGrouping(topLevelRootName); - section.setWithinAMap(section.isWithinAMap() || withinAMap); - section.setName(parentName + section.getName()); - List configDocItems = decorateGroupItems( - section.getConfigDocItems(), - configPhase, - topLevelRootName, - parentName, - additionalNames, - section.isWithinAMap(), - generateSeparateConfigGroupDocs); - String configGroupType = section.getConfigGroupType(); - if (generateSeparateConfigGroupDocs) { - addConfigGroupItemToHolder(configDocItems, configGroupType); - } - - if (section.isShowSection()) { - decoratedItems.add(configDocItem); - } else { - decoratedItems.addAll(configDocItems); - } - } - } - - return decoratedItems; - } - - private void addConfigGroupItemToHolder(List configDocItems, String configGroupType) { - List previousConfigGroupConfigItems = holder.getConfigGroupConfigItems() - .get(configGroupType); - if (previousConfigGroupConfigItems == null) { - holder.addConfigGroupItems(configGroupType, configDocItems); - } else { - previousConfigGroupConfigItems.addAll(configDocItems); - } - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemScanner.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemScanner.java deleted file mode 100644 index e0d3d722b71ae5..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemScanner.java +++ /dev/null @@ -1,291 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigGroupDocFileName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigRootDocFileName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.getName; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.stream.Collectors; - -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.TypeElement; - -import io.quarkus.annotation.processor.Constants; - -final public class ConfigDocItemScanner { - private static final String IO_QUARKUS_TEST_EXTENSION_PACKAGE = "io.quarkus.extest."; - - private final Set configRoots = new HashSet<>(); - private final Map configGroupsToTypeElement = new HashMap<>(); - - private final FsMap allExtensionGeneratedDocs; - private final FsMap allConfigGroupGeneratedDocs; - private final FsMultiMap configurationRootsParExtensionFileName; - - public ConfigDocItemScanner() { - this.allExtensionGeneratedDocs = new FsMap(Constants.GENERATED_DOCS_PATH - .resolve("all-configuration-roots-generated-doc")); - this.allConfigGroupGeneratedDocs = new FsMap(Constants.GENERATED_DOCS_PATH - .resolve("all-configuration-groups-generated-doc")); - this.configurationRootsParExtensionFileName = new FsMultiMap(Constants.GENERATED_DOCS_PATH - .resolve("extensions-configuration-roots-list")); - - } - - /** - * Record configuration group. It will later be visited to find configuration items. - */ - public void addConfigGroups(TypeElement configGroup) { - String configGroupName = configGroup.getQualifiedName().toString(); - if (configGroupName.startsWith(IO_QUARKUS_TEST_EXTENSION_PACKAGE)) { - return; - } - - configGroupsToTypeElement.put(configGroupName, configGroup); - } - - /** - * Record a configuration root class. It will later be visited to find configuration items. - */ - public void addConfigRoot(final PackageElement pkg, TypeElement clazz) { - if (pkg.toString().startsWith(IO_QUARKUS_TEST_EXTENSION_PACKAGE)) { - return; - } - - String prefix = Constants.QUARKUS; - ConfigPhase configPhase = ConfigPhase.BUILD_TIME; - - for (AnnotationMirror annotationMirror : clazz.getAnnotationMirrors()) { - String annotationName = annotationMirror.getAnnotationType().toString(); - if (annotationName.equals(Constants.ANNOTATION_CONFIG_ROOT)) { - final Map elementValues = annotationMirror - .getElementValues(); - String name = Constants.HYPHENATED_ELEMENT_NAME; - for (Map.Entry entry : elementValues.entrySet()) { - final String key = entry.getKey().toString(); - final String value = entry.getValue().getValue().toString(); - if ("name()".equals(key)) { - name = value; - } else if ("phase()".equals(key)) { - configPhase = ConfigPhase.valueOf(value); - } else if ("prefix()".equals(key)) { - prefix = value; - } - } - - for (AnnotationMirror mirror : clazz.getAnnotationMirrors()) { - if (mirror.getAnnotationType().toString().equals(Constants.ANNOTATION_CONFIG_MAPPING)) { - name = Constants.EMPTY; - for (Entry entry : mirror.getElementValues() - .entrySet()) { - if ("prefix()".equals(entry.getKey().toString())) { - prefix = entry.getValue().getValue().toString(); - } - } - } - } - - String docFileName = null; - for (AnnotationMirror mirror : clazz.getAnnotationMirrors()) { - if (mirror.getAnnotationType().toString().equals(Constants.ANNOTATION_CONFIG_DOC_FILE_NAME)) { - for (Entry entry : mirror.getElementValues() - .entrySet()) { - if ("value()".equals(entry.getKey().toString())) { - docFileName = entry.getValue().getValue().toString(); - break; - } - } - break; - } - } - - name = getName(prefix, name, clazz.getSimpleName().toString(), configPhase); - if (name.endsWith(Constants.DOT + Constants.PARENT)) { - // take into account the root case which would contain characters that can't be used to create the final file - name = name.replace(Constants.DOT + Constants.PARENT, ""); - } - - if (docFileName == null || docFileName.isEmpty()) { - final Matcher pkgMatcher = Constants.PKG_PATTERN.matcher(pkg.toString()); - if (pkgMatcher.find()) { - docFileName = DocGeneratorUtil.computeExtensionDocFileName(clazz.toString()); - } else { - docFileName = name.replace(Constants.DOT, Constants.DASH.charAt(0)) - + Constants.ADOC_EXTENSION; - } - } - ConfigRootInfo configRootInfo = new ConfigRootInfo(name, clazz, configPhase, docFileName); - configRoots.add(configRootInfo); - break; - } - } - } - - public Set scanExtensionsConfigurationItems(Properties javaDocProperties, boolean configMapping) - throws IOException { - - Set configDocGeneratedOutputs = new HashSet<>(); - final ConfigDocItemFinder configDocItemFinder = new ConfigDocItemFinder(configRoots, configGroupsToTypeElement, - javaDocProperties, allConfigGroupGeneratedDocs, allExtensionGeneratedDocs, configMapping); - final ScannedConfigDocsItemHolder inMemoryScannedItemsHolder = configDocItemFinder.findInMemoryConfigurationItems(); - - if (!inMemoryScannedItemsHolder.isEmpty()) { - updateScannedExtensionArtifactFiles(inMemoryScannedItemsHolder); - } - - Set allConfigItemsPerExtension = generateAllConfigItemsOutputs(inMemoryScannedItemsHolder); - Set configGroupConfigItems = generateAllConfigGroupOutputs(inMemoryScannedItemsHolder); - Set configRootConfigItems = generateAllConfigRootOutputs(inMemoryScannedItemsHolder); - - configDocGeneratedOutputs.addAll(configGroupConfigItems); - configDocGeneratedOutputs.addAll(allConfigItemsPerExtension); - configDocGeneratedOutputs.addAll(configRootConfigItems); - - return configDocGeneratedOutputs; - } - - /** - * Loads the list of configuration items per configuration root - * - */ - private Properties loadAllExtensionConfigItemsParConfigRoot() { - return allExtensionGeneratedDocs.asProperties(); - } - - /** - * Update extensions config roots. We need to gather the complete list of configuration roots of an extension - * when generating the documentation. - * - */ - private void updateConfigurationRootsList(Map.Entry> entry) { - String extensionFileName = entry.getKey().getFileName(); - String clazz = entry.getKey().getClazz().getQualifiedName().toString(); - configurationRootsParExtensionFileName.put(extensionFileName, clazz); - } - - private void updateScannedExtensionArtifactFiles(ScannedConfigDocsItemHolder inMemoryScannedItemsHolder) - throws IOException { - - for (Map.Entry> entry : inMemoryScannedItemsHolder.getConfigRootConfigItems() - .entrySet()) { - String serializableConfigRootDoc = Constants.OBJECT_MAPPER.writeValueAsString(entry.getValue()); - String clazz = entry.getKey().getClazz().getQualifiedName().toString(); - allExtensionGeneratedDocs.put(clazz, serializableConfigRootDoc); - updateConfigurationRootsList(entry); - } - - } - - private Set generateAllConfigItemsOutputs(ScannedConfigDocsItemHolder inMemoryScannedItemsHolder) - throws IOException { - Set outputs = new HashSet<>(); - - Set extensionFileNamesToGenerate = inMemoryScannedItemsHolder - .getConfigRootConfigItems() - .keySet() - .stream() - .map(ConfigRootInfo::getFileName) - .collect(Collectors.toSet()); - - for (String extensionFileName : extensionFileNamesToGenerate) { - List extensionConfigItems = new ArrayList<>(); - - for (String configRoot : configurationRootsParExtensionFileName.get(extensionFileName)) { - - List configDocItems = inMemoryScannedItemsHolder.getConfigItemsByRootClassName(configRoot); - if (configDocItems == null) { - String serializedContent = allExtensionGeneratedDocs.get(configRoot); - configDocItems = Constants.OBJECT_MAPPER.readValue(serializedContent, - Constants.LIST_OF_CONFIG_ITEMS_TYPE_REF); - } - - DocGeneratorUtil.appendConfigItemsIntoExistingOnes(extensionConfigItems, configDocItems); - } - - outputs.add(new ConfigDocGeneratedOutput(extensionFileName, true, extensionConfigItems, true)); - - List generalConfigItems = extensionConfigItems - .stream() - .filter(ConfigDocItem::isWithinAConfigGroup) - .collect(Collectors.toList()); - - if (!generalConfigItems.isEmpty()) { - String fileName = extensionFileName.replaceAll("\\.adoc$", "-general-config-items.adoc"); - outputs.add(new ConfigDocGeneratedOutput(fileName, false, generalConfigItems, true)); - } - - } - - return outputs; - } - - private Set generateAllConfigGroupOutputs( - ScannedConfigDocsItemHolder inMemoryScannedItemsHolder) { - - return inMemoryScannedItemsHolder - .getConfigGroupConfigItems() - .entrySet() - .stream() - .map(entry -> new ConfigDocGeneratedOutput(computeConfigGroupDocFileName(entry.getKey()), false, - entry.getValue(), true)) - .collect(Collectors.toSet()); - } - - private Set generateAllConfigRootOutputs(ScannedConfigDocsItemHolder inMemoryScannedItemsHolder) { - Set outputs = new HashSet<>(); - for (ConfigRootInfo configRootInfo : configRoots) { - String clazz = configRootInfo.getClazz().getQualifiedName().toString(); - List configDocItems = inMemoryScannedItemsHolder.getConfigItemsByRootClassName(clazz); - String fileName = computeConfigRootDocFileName(clazz, configRootInfo.getName()); - outputs.add(new ConfigDocGeneratedOutput(fileName, false, configDocItems, true)); - } - - return outputs; - } - - /** - * Return a Map structure which contains extension name as key and generated doc value. - */ - public Map> loadAllExtensionsConfigurationItems() - throws IOException { - - final Properties allExtensionGeneratedDocs = loadAllExtensionConfigItemsParConfigRoot(); - - final Map> foundExtensionConfigurationItems = new HashMap<>(); - - for (Entry entry : allExtensionGeneratedDocs.entrySet()) { - - final String serializedContent = (String) entry.getValue(); - if (serializedContent == null) { - continue; - } - - List configDocItems = Constants.OBJECT_MAPPER.readValue(serializedContent, - Constants.LIST_OF_CONFIG_ITEMS_TYPE_REF); - - foundExtensionConfigurationItems.put((String) entry.getKey(), configDocItems); - } - - return foundExtensionConfigurationItems; - } - - @Override - public String toString() { - return "ConfigDocItemScanner{" + - "configRoots=" + configRoots + - ", configGroups=" + configGroupsToTypeElement + - '}'; - } -} 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 deleted file mode 100644 index 7c021425621eb7..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java +++ /dev/null @@ -1,261 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.regex.Matcher; - -import io.quarkus.annotation.processor.Constants; - -final public class ConfigDocKey implements ConfigDocElement, Comparable { - private String type; - private String key; - private List additionalKeys = new ArrayList<>(); - private String configDoc; - private boolean withinAMap; - private String defaultValue; - private String javaDocSiteLink; - private String docMapKey; - private ConfigPhase configPhase; - private List acceptedValues; - private boolean optional; - private boolean list; - private boolean withinAConfigGroup; - // if a key is "quarkus.kubernetes.part-of", then the value of this would be "kubernetes" - private String topLevelGrouping; - private boolean isEnum; - private String since; - private String environmentVariable; - - public ConfigDocKey() { - } - - public boolean hasType() { - return type != null && !type.isEmpty(); - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public boolean hasAcceptedValues() { - return acceptedValues != null && !acceptedValues.isEmpty(); - } - - public List getAcceptedValues() { - return acceptedValues; - } - - public void setAcceptedValues(List acceptedValues) { - this.acceptedValues = acceptedValues; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public List getAdditionalKeys() { - return additionalKeys; - } - - public void setAdditionalKeys(final List additionalKeys) { - this.additionalKeys = additionalKeys; - } - - public void setTopLevelGrouping(String topLevelGrouping) { - this.topLevelGrouping = topLevelGrouping; - } - - public String getConfigDoc() { - return configDoc; - } - - public void setConfigDoc(String configDoc) { - this.configDoc = configDoc; - } - - public String getJavaDocSiteLink() { - if (javaDocSiteLink == null) { - return Constants.EMPTY; - } - - return javaDocSiteLink; - } - - public void setJavaDocSiteLink(String javaDocSiteLink) { - this.javaDocSiteLink = javaDocSiteLink; - } - - public String getDefaultValue() { - if (!defaultValue.isEmpty()) { - return defaultValue; - } - - final String defaultValue = DocGeneratorUtil.getPrimitiveDefaultValue(type); - - if (defaultValue == null) { - return Constants.EMPTY; - } - - return defaultValue; - } - - public void setDefaultValue(String defaultValue) { - this.defaultValue = defaultValue; - } - - public ConfigPhase getConfigPhase() { - return configPhase; - } - - public void setConfigPhase(ConfigPhase configPhase) { - this.configPhase = configPhase; - } - - public void setWithinAMap(boolean withinAMap) { - this.withinAMap = withinAMap; - } - - @SuppressWarnings("unused") - public boolean isWithinAMap() { - return withinAMap; - } - - public String computeTypeSimpleName() { - String unwrappedType = DocGeneratorUtil.unbox(type); - - Matcher matcher = Constants.CLASS_NAME_PATTERN.matcher(unwrappedType); - if (matcher.find()) { - return matcher.group(1); - } - - return unwrappedType; - } - - public void setOptional(boolean optional) { - this.optional = optional; - } - - public boolean isOptional() { - return optional; - } - - public void setList(boolean list) { - this.list = list; - } - - public boolean isList() { - return list; - } - - public String getDocMapKey() { - return docMapKey; - } - - public void setDocMapKey(String docMapKey) { - this.docMapKey = docMapKey; - } - - public boolean isWithinAConfigGroup() { - return withinAConfigGroup; - } - - public void setWithinAConfigGroup(boolean withinAConfigGroup) { - this.withinAConfigGroup = withinAConfigGroup; - } - - public String getTopLevelGrouping() { - return topLevelGrouping; - } - - public boolean isEnum() { - return isEnum; - } - - public void setEnum(boolean anEnum) { - isEnum = anEnum; - } - - public String getSince() { - return since; - } - - public void setSince(String since) { - this.since = since; - } - - public String getEnvironmentVariable() { - return environmentVariable; - } - - public void setEnvironmentVariable(String environmentVariable) { - this.environmentVariable = environmentVariable; - } - - @Override - public void accept(Writer writer, DocFormatter docFormatter) throws IOException { - docFormatter.format(writer, this); - } - - @Override - public int compareTo(ConfigDocElement o) { - return compare(o); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - ConfigDocKey that = (ConfigDocKey) o; - return withinAMap == that.withinAMap && - optional == that.optional && - list == that.list && - withinAConfigGroup == that.withinAConfigGroup && - Objects.equals(type, that.type) && - Objects.equals(key, that.key) && - Objects.equals(configDoc, that.configDoc) && - Objects.equals(defaultValue, that.defaultValue) && - Objects.equals(javaDocSiteLink, that.javaDocSiteLink) && - Objects.equals(docMapKey, that.docMapKey) && - configPhase == that.configPhase && - Objects.equals(acceptedValues, that.acceptedValues) && - Objects.equals(topLevelGrouping, that.topLevelGrouping); - } - - @Override - public int hashCode() { - return Objects.hash(type, key, configDoc, withinAMap, defaultValue, javaDocSiteLink, docMapKey, configPhase, - acceptedValues, optional, list, withinAConfigGroup, topLevelGrouping); - } - - @Override - public String toString() { - return "ConfigDocKey{" + - "type='" + type + '\'' + - ", key='" + key + '\'' + - ", configDoc='" + configDoc + '\'' + - ", withinAMap=" + withinAMap + - ", defaultValue='" + defaultValue + '\'' + - ", javaDocSiteLink='" + javaDocSiteLink + '\'' + - ", docMapKey='" + docMapKey + '\'' + - ", configPhase=" + configPhase + - ", acceptedValues=" + acceptedValues + - ", optional=" + optional + - ", list=" + list + - ", withinAConfigGroup=" + withinAConfigGroup + - ", topLevelGrouping='" + topLevelGrouping + '\'' + - '}'; - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocSection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocSection.java deleted file mode 100644 index ff04da68138314..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocSection.java +++ /dev/null @@ -1,173 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -final public class ConfigDocSection implements ConfigDocElement, Comparable { - private String name; - private boolean optional; - private boolean withinAMap; - private String sectionDetails; - private String sectionDetailsTitle; - private ConfigPhase configPhase; - private String topLevelGrouping; - private String configGroupType; - private boolean showSection; - - private List configDocItems = new ArrayList<>(); - private String anchorPrefix; - - public ConfigDocSection() { - } - - public String getConfigGroupType() { - return configGroupType; - } - - public void setConfigGroupType(String configGroupType) { - this.configGroupType = configGroupType; - } - - public boolean isShowSection() { - return showSection; - } - - public void setShowSection(boolean showSection) { - this.showSection = showSection; - } - - public boolean isWithinAMap() { - return withinAMap; - } - - public void setWithinAMap(boolean withinAMap) { - this.withinAMap = withinAMap; - } - - public ConfigPhase getConfigPhase() { - return configPhase; - } - - public void setConfigPhase(ConfigPhase configPhase) { - this.configPhase = configPhase; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getSectionDetails() { - return sectionDetails; - } - - public void setSectionDetails(String sectionDetails) { - this.sectionDetails = sectionDetails; - } - - public String getSectionDetailsTitle() { - return sectionDetailsTitle; - } - - public void setSectionDetailsTitle(String sectionDetailsTitle) { - this.sectionDetailsTitle = sectionDetailsTitle; - } - - public List getConfigDocItems() { - return configDocItems; - } - - public void setConfigDocItems(List configDocItems) { - this.configDocItems = configDocItems; - } - - public void addConfigDocItems(List configDocItems) { - this.configDocItems.addAll(configDocItems); - } - - @Override - public void accept(Writer writer, DocFormatter docFormatter) throws IOException { - docFormatter.format(writer, this); - } - - @Override - public int compareTo(ConfigDocElement o) { - return compare(o); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - ConfigDocSection that = (ConfigDocSection) o; - return sectionDetailsTitle.equals(that.sectionDetailsTitle); - } - - @Override - public int hashCode() { - return Objects.hash(sectionDetailsTitle); - } - - @Override - public String toString() { - return "ConfigDocSection{" + - "name='" + name + '\'' + - ", optional=" + optional + - ", withinAMap=" + withinAMap + - ", sectionDetails='" + sectionDetails + '\'' + - ", sectionDetailsTitle='" + sectionDetailsTitle + '\'' + - ", configPhase=" + configPhase + - ", topLevelGrouping='" + topLevelGrouping + '\'' + - ", configDocItems=" + configDocItems + - ", anchorPrefix='" + anchorPrefix + '\'' + - '}'; - } - - public boolean hasDurationInformationNote() { - for (ConfigDocItem item : configDocItems) { - if (item.hasDurationInformationNote()) - return true; - } - return false; - } - - public boolean hasMemoryInformationNote() { - for (ConfigDocItem item : configDocItems) { - if (item.hasMemoryInformationNote()) - return true; - } - return false; - } - - public void setAnchorPrefix(String anchorPrefix) { - this.anchorPrefix = anchorPrefix; - } - - public String getAnchorPrefix() { - return anchorPrefix; - } - - public boolean isOptional() { - return optional; - } - - public void setOptional(boolean optional) { - this.optional = optional; - } - - public String getTopLevelGrouping() { - return topLevelGrouping; - } - - public void setTopLevelGrouping(String topLevelGrouping) { - this.topLevelGrouping = topLevelGrouping; - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocWriter.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocWriter.java deleted file mode 100644 index b2baf426709a3e..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocWriter.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.io.Writer; -import java.nio.file.Files; -import java.nio.file.Path; - -import io.quarkus.annotation.processor.Constants; - -final public class ConfigDocWriter { - - /** - * Write all extension configuration in AsciiDoc format in `{root}/target/asciidoc/generated/config/` directory - */ - public void writeAllExtensionConfigDocumentation(ConfigDocGeneratedOutput output) - throws IOException { - - if (output.getConfigDocItems().isEmpty()) { - return; - } - - // Create single summary table - final var configDocBuilder = new ConfigDocBuilder().addSummaryTable(output.getAnchorPrefix(), output.isSearchable(), - output.getConfigDocItems(), output.getFileName(), true); - - generateDocumentation(output.getFileName(), configDocBuilder); - } - - public void generateDocumentation(String fileName, ConfigDocBuilder configDocBuilder) throws IOException { - generateDocumentation( - // Resolve output file path - Constants.GENERATED_DOCS_PATH.resolve(fileName), - // Write all items - configDocBuilder.build()); - } - - private void generateDocumentation(Path targetPath, ConfigDoc configDoc) - throws IOException { - try (Writer writer = Files.newBufferedWriter(targetPath)) { - for (ConfigDoc.WriteItem writeItem : configDoc.getWriteItems()) { - // Write documentation item, f.e. summary table - writeItem.accept(writer); - } - } - } - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigPhase.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigPhase.java deleted file mode 100644 index 5120966ee22800..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigPhase.java +++ /dev/null @@ -1,80 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.util.Comparator; - -import io.quarkus.annotation.processor.Constants; - -public enum ConfigPhase implements Comparable { - RUN_TIME("The configuration is overridable at runtime", "", "RunTime"), - BUILD_TIME("The configuration is not overridable at runtime", Constants.CONFIG_PHASE_BUILD_TIME_ILLUSTRATION, "BuildTime"), - BUILD_AND_RUN_TIME_FIXED("The configuration is not overridable at runtime", Constants.CONFIG_PHASE_BUILD_TIME_ILLUSTRATION, - "BuildTime"); - - static final Comparator COMPARATOR = new Comparator() { - /** - * Order built time phase first - * Then build time run time fixed phase - * Then runtime one - */ - @Override - public int compare(ConfigPhase firstPhase, ConfigPhase secondPhase) { - switch (firstPhase) { - case BUILD_TIME: { - switch (secondPhase) { - case BUILD_TIME: - return 0; - default: - return -1; - } - } - case BUILD_AND_RUN_TIME_FIXED: { - switch (secondPhase) { - case BUILD_TIME: - return 1; - case BUILD_AND_RUN_TIME_FIXED: - return 0; - default: - return -1; - } - } - case RUN_TIME: { - switch (secondPhase) { - case RUN_TIME: - return 0; - default: - return 1; - } - } - default: - return 0; - } - } - }; - - private String description; - private String illustration; - private String configSuffix; - - ConfigPhase(String description, String illustration, String configSuffix) { - this.description = description; - this.illustration = illustration; - this.configSuffix = configSuffix; - } - - @Override - public String toString() { - return "ConfigPhase{" + - "description='" + description + '\'' + - ", illustration='" + illustration + '\'' + - ", configSuffix='" + configSuffix + '\'' + - '}'; - } - - public String getIllustration() { - return illustration; - } - - public String getConfigSuffix() { - return configSuffix; - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigRootInfo.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigRootInfo.java deleted file mode 100644 index 786c07fc871863..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigRootInfo.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.util.Objects; - -import javax.lang.model.element.TypeElement; - -final public class ConfigRootInfo { - private final String name; - private final TypeElement clazz; - private final ConfigPhase configPhase; - private final String fileName; - - public ConfigRootInfo( - final String name, - final TypeElement clazz, - final ConfigPhase configPhase, - final String fileName) { - this.name = name; - this.clazz = clazz; - this.configPhase = configPhase; - this.fileName = fileName; - } - - public String getFileName() { - return fileName; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final ConfigRootInfo that = (ConfigRootInfo) o; - return name.equals(that.name) && - clazz.equals(that.clazz) && - configPhase == that.configPhase && - fileName.equals(that.fileName); - } - - @Override - public int hashCode() { - return Objects.hash(name, clazz, configPhase, fileName); - } - - @Override - public String toString() { - return "ConfigRootInfo{" + - "name='" + name + '\'' + - ", clazz=" + clazz + - ", configPhase=" + configPhase + - ", fileName='" + fileName + '\'' + - '}'; - } - - public String getName() { - return name; - } - - public TypeElement getClazz() { - return clazz; - } - - public ConfigPhase getConfigPhase() { - return configPhase; - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocFormatter.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocFormatter.java deleted file mode 100644 index e153f1aa0afc63..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocFormatter.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.io.Writer; -import java.text.Normalizer; -import java.util.List; - -interface DocFormatter { - default String getAnchor(String string) { - // remove accents - string = Normalizer.normalize(string, Normalizer.Form.NFKC) - .replaceAll("[àáâãäåāąă]", "a") - .replaceAll("[çćčĉċ]", "c") - .replaceAll("[ďđð]", "d") - .replaceAll("[èéêëēęěĕė]", "e") - .replaceAll("[ƒſ]", "f") - .replaceAll("[ĝğġģ]", "g") - .replaceAll("[ĥħ]", "h") - .replaceAll("[ìíîïīĩĭįı]", "i") - .replaceAll("[ijĵ]", "j") - .replaceAll("[ķĸ]", "k") - .replaceAll("[łľĺļŀ]", "l") - .replaceAll("[ñńňņʼnŋ]", "n") - .replaceAll("[òóôõöøōőŏœ]", "o") - .replaceAll("[Þþ]", "p") - .replaceAll("[ŕřŗ]", "r") - .replaceAll("[śšşŝș]", "s") - .replaceAll("[ťţŧț]", "t") - .replaceAll("[ùúûüūůűŭũų]", "u") - .replaceAll("[ŵ]", "w") - .replaceAll("[ýÿŷ]", "y") - .replaceAll("[žżź]", "z") - .replaceAll("[æ]", "ae") - .replaceAll("[ÀÁÂÃÄÅĀĄĂ]", "A") - .replaceAll("[ÇĆČĈĊ]", "C") - .replaceAll("[ĎĐÐ]", "D") - .replaceAll("[ÈÉÊËĒĘĚĔĖ]", "E") - .replaceAll("[ĜĞĠĢ]", "G") - .replaceAll("[ĤĦ]", "H") - .replaceAll("[ÌÍÎÏĪĨĬĮİ]", "I") - .replaceAll("[Ĵ]", "J") - .replaceAll("[Ķ]", "K") - .replaceAll("[ŁĽĹĻĿ]", "L") - .replaceAll("[ÑŃŇŅŊ]", "N") - .replaceAll("[ÒÓÔÕÖØŌŐŎ]", "O") - .replaceAll("[ŔŘŖ]", "R") - .replaceAll("[ŚŠŞŜȘ]", "S") - .replaceAll("[ÙÚÛÜŪŮŰŬŨŲ]", "U") - .replaceAll("[Ŵ]", "W") - .replaceAll("[ÝŶŸ]", "Y") - .replaceAll("[ŹŽŻ]", "Z") - .replaceAll("[ß]", "ss"); - - // Apostrophes. - string = string.replaceAll("([a-z])'s([^a-z])", "$1s$2"); - // Allow only letters, -, _ - string = string.replaceAll("[^\\w-_]", "-").replaceAll("-{2,}", "-"); - // Get rid of any - at the start and end. - string = string.replaceAll("-+$", "").replaceAll("^-+", ""); - - return string.toLowerCase(); - } - - void format(Writer writer, String initialAnchorPrefix, boolean activateSearch, List configDocItems, - boolean includeConfigPhaseLegend) throws IOException; - - void format(Writer writer, ConfigDocKey configDocKey) throws IOException; - - void format(Writer writer, ConfigDocSection configDocSection) throws IOException; -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtil.java deleted file mode 100644 index 95a43d1feac346..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtil.java +++ /dev/null @@ -1,534 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; - -import io.quarkus.annotation.processor.Constants; - -public class DocGeneratorUtil { - private static final String NEW_LINE = "\n"; - private static final String CORE = "core"; - private static final String CONFIG = "Config"; - private static final String CONFIGURATION = "Configuration"; - public static final String LEVEL_HACK_URL = "https://docs.jboss.org/jbossas/javadoc/7.1.2.Final/org/jboss/logmanager/Level.html"; - private static String CONFIG_GROUP_DOC_PREFIX = "config-group-"; - static final String VERTX_JAVA_DOC_SITE = "https://vertx.io/docs/apidocs/"; - static final String OFFICIAL_JAVA_DOC_BASE_LINK = "https://docs.oracle.com/javase/8/docs/api/"; - static final String AGROAL_API_JAVA_DOC_SITE = "https://jar-download.com/javaDoc/io.agroal/agroal-api/1.5/index.html?"; - - private static final Map JAVA_PRIMITIVE_WRAPPERS = new HashMap<>(); - private static final Map PRIMITIVE_DEFAULT_VALUES = new HashMap<>(); - private static final Map EXTENSION_JAVA_DOC_LINK = new HashMap<>(); - private static Pattern PACKAGE_PATTERN = Pattern.compile("^(\\w+)\\.(\\w+)\\..*$"); - private static final String HYPHEN = "-"; - private static final Pattern PATTERN = Pattern.compile("([-_]+)"); - - static { - PRIMITIVE_DEFAULT_VALUES.put("int", "0"); - PRIMITIVE_DEFAULT_VALUES.put("byte", "0"); - PRIMITIVE_DEFAULT_VALUES.put("char", ""); - PRIMITIVE_DEFAULT_VALUES.put("short", "0"); - PRIMITIVE_DEFAULT_VALUES.put("long", "0l"); - PRIMITIVE_DEFAULT_VALUES.put("float", "0f"); - PRIMITIVE_DEFAULT_VALUES.put("double", "0d"); - PRIMITIVE_DEFAULT_VALUES.put("boolean", "false"); - - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Character", "char"); - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Boolean", "boolean"); - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Byte", "byte"); - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Short", "short"); - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Integer", "int"); - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Long", "long"); - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Float", "float"); - JAVA_PRIMITIVE_WRAPPERS.put("java.lang.Double", "double"); - - EXTENSION_JAVA_DOC_LINK.put("io.vertx.", VERTX_JAVA_DOC_SITE); - EXTENSION_JAVA_DOC_LINK.put("io.agroal.", AGROAL_API_JAVA_DOC_SITE); - } - - /** - * Retrieve a default value of a primitive type. - * If type is not a primitive, returns false - * - */ - static String getPrimitiveDefaultValue(String primitiveType) { - return PRIMITIVE_DEFAULT_VALUES.get(primitiveType); - } - - /** - * Replaces Java primitive wrapper types with primitive types - */ - static String unbox(String type) { - String mapping = JAVA_PRIMITIVE_WRAPPERS.get(type); - return mapping == null ? type : mapping; - } - - /** - * Get javadoc link of a given type value - */ - static String getJavaDocSiteLink(String type) { - if (type.equals(Level.class.getName())) { - //hack, we don't want to link to the JUL version, but the jboss logging version - //this seems like a one off use case so for now it is just hacked in here - //if there are other use cases we should do something more generic - return LEVEL_HACK_URL; - } - Matcher packageMatcher = PACKAGE_PATTERN.matcher(type); - - if (!packageMatcher.find()) { - return Constants.EMPTY; - } - - if (JAVA_PRIMITIVE_WRAPPERS.containsKey(type)) { - return Constants.EMPTY; - } - - if ("java".equals(packageMatcher.group(1))) { - return OFFICIAL_JAVA_DOC_BASE_LINK + getJavaDocLinkForType(type); - } - - String basePkgName = packageMatcher.group(1) + "." + packageMatcher.group(2) + "."; - String javaDocBaseUrl = EXTENSION_JAVA_DOC_LINK.get(basePkgName); - - if (javaDocBaseUrl != null) { - return javaDocBaseUrl + getJavaDocLinkForType(type); - } - - return Constants.EMPTY; - } - - private static String getJavaDocLinkForType(String type) { - int beginOfWrappedTypeIndex = type.indexOf("<"); - if (beginOfWrappedTypeIndex != -1) { - type = type.substring(0, beginOfWrappedTypeIndex); - } - - int indexOfFirstUpperCase = 0; - for (int index = 0; index < type.length(); index++) { - char charAt = type.charAt(index); - if (charAt >= 'A' && charAt <= 'Z') { - indexOfFirstUpperCase = index; - break; - } - } - - final String base = type.substring(0, indexOfFirstUpperCase).replace('.', '/'); - final String html = type.substring(indexOfFirstUpperCase).replace('$', '.') + ".html"; - - return base + html; - } - - /** - * Retrieve enclosed type from known optional types - */ - static String getKnownGenericType(DeclaredType declaredType) { - return Constants.ALIASED_TYPES.get(declaredType.toString()); - } - - static Iterator camelHumpsIterator(String str) { - return new Iterator() { - int idx; - - @Override - public boolean hasNext() { - return idx < str.length(); - } - - @Override - public String next() { - if (idx == str.length()) - throw new NoSuchElementException(); - // known mixed-case rule-breakers - if (str.startsWith("JBoss", idx)) { - idx += 5; - return "JBoss"; - } - final int start = idx; - int c = str.codePointAt(idx); - if (Character.isUpperCase(c)) { - // an uppercase-starting word - idx = str.offsetByCodePoints(idx, 1); - if (idx < str.length()) { - c = str.codePointAt(idx); - if (Character.isUpperCase(c)) { - // all-caps word; need one look-ahead - int nextIdx = str.offsetByCodePoints(idx, 1); - while (nextIdx < str.length()) { - c = str.codePointAt(nextIdx); - if (Character.isLowerCase(c)) { - // ended at idx - return str.substring(start, idx); - } - idx = nextIdx; - nextIdx = str.offsetByCodePoints(idx, 1); - } - // consumed the whole remainder, update idx to length - idx = str.length(); - return str.substring(start); - } else { - // initial caps, trailing lowercase - idx = str.offsetByCodePoints(idx, 1); - while (idx < str.length()) { - c = str.codePointAt(idx); - if (Character.isUpperCase(c)) { - // end - return str.substring(start, idx); - } - idx = str.offsetByCodePoints(idx, 1); - } - // consumed the whole remainder - return str.substring(start); - } - } else { - // one-letter word - return str.substring(start); - } - } else { - // a lowercase-starting word - idx = str.offsetByCodePoints(idx, 1); - while (idx < str.length()) { - c = str.codePointAt(idx); - if (Character.isUpperCase(c)) { - // end - return str.substring(start, idx); - } - idx = str.offsetByCodePoints(idx, 1); - } - // consumed the whole remainder - return str.substring(start); - } - } - }; - } - - static Iterator lowerCase(Iterator orig) { - return new Iterator() { - @Override - public boolean hasNext() { - return orig.hasNext(); - } - - @Override - public String next() { - return orig.next().toLowerCase(Locale.ROOT); - } - }; - } - - static String join(Iterator it) { - final StringBuilder b = new StringBuilder(); - if (it.hasNext()) { - b.append(it.next()); - while (it.hasNext()) { - b.append("-"); - b.append(it.next()); - } - } - return b.toString(); - } - - static String hyphenate(String orig) { - return join(lowerCase(camelHumpsIterator(orig))); - } - - /** - * This needs to be consistent with io.quarkus.runtime.configuration.HyphenateEnumConverter. - */ - static String hyphenateEnumValue(String orig) { - StringBuffer target = new StringBuffer(); - String hyphenate = hyphenate(orig); - Matcher matcher = PATTERN.matcher(hyphenate); - while (matcher.find()) { - matcher.appendReplacement(target, HYPHEN); - } - matcher.appendTail(target); - return target.toString(); - } - - static String normalizeDurationValue(String value) { - if (!value.isEmpty() && Character.isDigit(value.charAt(value.length() - 1))) { - try { - value = Integer.parseInt(value) + "S"; - } catch (NumberFormatException ignore) { - } - } - value = value.toUpperCase(Locale.ROOT); - return value; - } - - static String joinAcceptedValues(List acceptedValues) { - if (acceptedValues == null || acceptedValues.isEmpty()) { - return ""; - } - - return acceptedValues.stream().collect(Collectors.joining("`, `", Constants.CODE_DELIMITER, Constants.CODE_DELIMITER)); - } - - static String joinEnumValues(List enumValues) { - if (enumValues == null || enumValues.isEmpty()) { - return Constants.EMPTY; - } - - // nested macros are only detected when cell starts with a new line, e.g. a|\n myMacro::[] - return NEW_LINE + String.join(", ", enumValues); - } - - static String getTypeFormatInformationNote(ConfigDocKey configDocKey) { - if (configDocKey.getType().equals(Duration.class.getName())) { - return Constants.DURATION_INFORMATION; - } else if (configDocKey.getType().equals(Constants.MEMORY_SIZE_TYPE)) { - return Constants.MEMORY_SIZE_INFORMATION; - } - - return Constants.EMPTY; - } - - static boolean hasDurationInformationNote(ConfigDocKey configDocKey) { - return configDocKey.hasType() && configDocKey.getType().equals(Duration.class.getName()); - } - - static boolean hasMemoryInformationNote(ConfigDocKey configDocKey) { - return configDocKey.hasType() && configDocKey.getType().equals(Constants.MEMORY_SIZE_TYPE); - } - - /** - * Guess extension name from given configuration root class name - */ - public static String computeExtensionDocFileName(String configRoot) { - StringBuilder extensionNameBuilder = new StringBuilder(); - final Matcher matcher = Constants.PKG_PATTERN.matcher(configRoot); - if (!matcher.find()) { - extensionNameBuilder.append(configRoot); - } else { - String extensionName = matcher.group(1); - extensionNameBuilder.append(Constants.QUARKUS); - extensionNameBuilder.append(Constants.DASH); - - if (Constants.DEPLOYMENT.equals(extensionName) || Constants.RUNTIME.equals(extensionName)) { - extensionNameBuilder.append(CORE); - } else { - extensionNameBuilder.append(extensionName); - for (int i = 2; i <= matcher.groupCount(); i++) { - String subgroup = matcher.group(i); - if (Constants.DEPLOYMENT.equals(subgroup) - || Constants.RUNTIME.equals(subgroup) - || Constants.COMMON.equals(subgroup) - || !subgroup.matches(Constants.DIGIT_OR_LOWERCASE)) { - break; - } - if (i > 3 && Constants.CONFIG.equals(subgroup)) { - // this is a bit dark magic, but we have config packages as valid extension names - // and config packages where the configuration is stored - break; - } - extensionNameBuilder.append(Constants.DASH); - extensionNameBuilder.append(matcher.group(i)); - } - } - } - - extensionNameBuilder.append(Constants.ADOC_EXTENSION); - return extensionNameBuilder.toString(); - } - - /** - * Guess config group file name from given configuration group class name - */ - public static String computeConfigGroupDocFileName(String configGroupClassName) { - final String sanitizedClassName; - final Matcher matcher = Constants.PKG_PATTERN.matcher(configGroupClassName); - - if (!matcher.find()) { - sanitizedClassName = CONFIG_GROUP_DOC_PREFIX + Constants.DASH + hyphenate(configGroupClassName); - } else { - String replacement = Constants.DASH + CONFIG_GROUP_DOC_PREFIX + Constants.DASH; - sanitizedClassName = configGroupClassName - .replaceFirst("io.", "") - .replaceFirst("\\.runtime\\.", replacement) - .replaceFirst("\\.deployment\\.", replacement); - } - - return hyphenate(sanitizedClassName) - .replaceAll("[\\.-]+", Constants.DASH) - + Constants.ADOC_EXTENSION; - } - - /** - * Guess config root file name from given configuration root class name. - */ - public static String computeConfigRootDocFileName(String configRootClassName, String rootName) { - String sanitizedClassName; - final Matcher matcher = Constants.PKG_PATTERN.matcher(configRootClassName); - - if (!matcher.find()) { - sanitizedClassName = rootName + Constants.DASH + hyphenate(configRootClassName); - } else { - String deployment = Constants.DOT + Constants.DEPLOYMENT + Constants.DOT; - String runtime = Constants.DOT + Constants.RUNTIME + Constants.DOT; - - if (configRootClassName.contains(deployment)) { - sanitizedClassName = configRootClassName - .substring(configRootClassName.indexOf(deployment) + deployment.length()); - } else if (configRootClassName.contains(runtime)) { - sanitizedClassName = configRootClassName.substring(configRootClassName.indexOf(runtime) + runtime.length()); - } else { - sanitizedClassName = configRootClassName.replaceFirst("io.quarkus.", ""); - } - - sanitizedClassName = rootName + Constants.DASH + sanitizedClassName; - } - - return hyphenate(sanitizedClassName) - .replaceAll("[\\.-]+", Constants.DASH) - + Constants.ADOC_EXTENSION; - } - - public static void appendConfigItemsIntoExistingOnes(List existingConfigItems, - List configDocItems) { - for (ConfigDocItem configDocItem : configDocItems) { - if (configDocItem.isConfigKey()) { - existingConfigItems.add(configDocItem); - } else { - ConfigDocSection configDocSection = configDocItem.getConfigDocSection(); - boolean configSectionMerged = mergeSectionIntoPreviousExistingConfigItems(configDocSection, - existingConfigItems); - if (!configSectionMerged) { - existingConfigItems.add(configDocItem); - } - } - } - } - - /** - * returns true if section is merged into one of the existing config items, false otherwise - */ - private static boolean mergeSectionIntoPreviousExistingConfigItems(ConfigDocSection section, - List configDocItems) { - for (ConfigDocItem configDocItem : configDocItems) { - if (configDocItem.isConfigKey()) { - continue; - } - - ConfigDocSection configDocSection = configDocItem.getConfigDocSection(); - if (configDocSection.equals(section)) { - appendConfigItemsIntoExistingOnes(configDocSection.getConfigDocItems(), section.getConfigDocItems()); - return true; - } else { - boolean configSectionMerged = mergeSectionIntoPreviousExistingConfigItems(section, - configDocSection.getConfigDocItems()); - if (configSectionMerged) { - return true; - } - } - } - - return false; - } - - static String stringifyType(TypeMirror typeMirror) { - List typeArguments = ((DeclaredType) typeMirror).getTypeArguments(); - String simpleName = typeSimpleName(typeMirror); - if (typeArguments.isEmpty()) { - return simpleName; - } else if (typeArguments.size() == 1) { - return String.format("%s<%s>", simpleName, stringifyType(typeArguments.get(0))); - } else if (typeArguments.size() == 2) { - return String.format("%s<%s,%s>", simpleName, stringifyType(typeArguments.get(0)), - stringifyType(typeArguments.get(1))); - } - - return "unknown"; // we should not reach here - } - - private static String typeSimpleName(TypeMirror typeMirror) { - String type = ((DeclaredType) typeMirror).asElement().toString(); - return type.substring(1 + type.lastIndexOf(Constants.DOT)); - } - - static String getName(String prefix, String name, String simpleClassName, ConfigPhase configPhase) { - if (name.equals(Constants.HYPHENATED_ELEMENT_NAME)) { - return deriveConfigRootName(simpleClassName, prefix, configPhase); - } - - if (!prefix.isEmpty()) { - if (!name.isEmpty()) { - return prefix + Constants.DOT + name; - } else { - return prefix; - } - } else { - return name; - } - } - - /** - * Replace each character that is neither alphanumeric nor _ with _ then convert the name to upper case, e.g. - * quarkus.datasource.jdbc.initial-size -> QUARKUS_DATASOURCE_JDBC_INITIAL_SIZE - * See also: io.smallrye.config.common.utils.StringUtil#replaceNonAlphanumericByUnderscores(java.lang.String) - */ - static String toEnvVarName(final String name) { - int length = name.length(); - StringBuilder sb = new StringBuilder(length); - for (int i = 0; i < length; i++) { - char c = name.charAt(i); - if ('a' <= c && c <= 'z' || - 'A' <= c && c <= 'Z' || - '0' <= c && c <= '9') { - sb.append(c); - } else { - sb.append('_'); - } - } - return sb.toString().toUpperCase(); - } - - static String deriveConfigRootName(String simpleClassName, String prefix, ConfigPhase configPhase) { - String simpleNameInLowerCase = simpleClassName.toLowerCase(); - int length = simpleNameInLowerCase.length(); - - if (simpleNameInLowerCase.endsWith(CONFIG.toLowerCase())) { - String sanitized = simpleClassName.substring(0, length - CONFIG.length()); - return deriveConfigRootName(sanitized, prefix, configPhase); - } else if (simpleNameInLowerCase.endsWith(CONFIGURATION.toLowerCase())) { - String sanitized = simpleClassName.substring(0, length - CONFIGURATION.length()); - return deriveConfigRootName(sanitized, prefix, configPhase); - } else if (simpleNameInLowerCase.endsWith(configPhase.getConfigSuffix().toLowerCase())) { - String sanitized = simpleClassName.substring(0, length - configPhase.getConfigSuffix().length()); - return deriveConfigRootName(sanitized, prefix, configPhase); - } - - return !prefix.isEmpty() ? prefix + Constants.DOT + hyphenate(simpleClassName) - : Constants.QUARKUS + Constants.DOT + hyphenate(simpleClassName); - } - - /** - * Sort docs keys. The sorted list will contain the properties in the following order - * - 1. Map config items as last elements of the generated docs. - * - 2. Build time properties will come first. - * - 3. Otherwise, respect source code declaration order. - * - 4. Elements within a configuration section will appear at the end of the generated doc while preserving described in - * 1-4. - */ - public static void sort(List configDocItems) { - Collections.sort(configDocItems); - for (ConfigDocItem configDocItem : configDocItems) { - if (configDocItem.isConfigSection()) { - sort(configDocItem.getConfigDocSection().getConfigDocItems()); - } - } - } - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/FsMap.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/FsMap.java deleted file mode 100644 index 2cb0492bd84301..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/FsMap.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Properties; -import java.util.stream.Stream; - -/** - * A file system backed map. - */ -public class FsMap { - - private final Path dir; - - public FsMap(Path dir) { - this.dir = safeCreateDirectories(dir); - } - - public String get(String key) { - final Path file = dir.resolve(key); - if (Files.exists(file)) { - try { - return Files.readString(file); - } catch (IOException e) { - throw new RuntimeException("Could not read " + file, e); - } - } else { - return null; - } - } - - public void put(String key, String value) { - final Path file = dir.resolve(key); - try { - Files.write(file, value.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException("Could not write " + file, e); - } - } - - public boolean hasKey(String key) { - final Path file = dir.resolve(key); - return Files.exists(file); - } - - /** - * Attempts to call {@link Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...)} with the given - * {@code dir} at most {@code dir.getNameCount()} times as long as it does not exist, assuming that other treads - * may try to create the same directory concurrently. - * - * @param dir the directory to create - * @throws RuntimeException A wrapped {@link IOException} thrown by the last call to - * {@link Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...)} - */ - static Path safeCreateDirectories(Path dir) { - IOException lastException; - int retries = dir.getNameCount(); - do { - if (Files.exists(dir)) { - return dir; - } - try { - Files.createDirectories(dir); - return dir; - } catch (IOException e) { - lastException = e; - } - } while (retries-- > 0); - throw new RuntimeException("Could not create directories " + dir, lastException); - } - - public Properties asProperties() { - final Properties result = new Properties(); - if (Files.exists(dir)) { - try (Stream files = Files.list(dir)) { - files - .filter(Files::isRegularFile) - .forEach(f -> { - try { - result.setProperty(f.getFileName().toString(), - Files.readString(f)); - } catch (IOException e) { - throw new IllegalStateException("Could not read from " + f, e); - } - }); - } catch (IOException e) { - throw new RuntimeException("Could not list " + dir, e); - } - } - return result; - } - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/FsMultiMap.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/FsMultiMap.java deleted file mode 100644 index b7842182161a11..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/FsMultiMap.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * A file system backed map associating each key with a collection of values - */ -public class FsMultiMap { - - private final Path dir; - - public FsMultiMap(Path dir) { - this.dir = FsMap.safeCreateDirectories(dir); - } - - /** - * @param key - * @return the collection associated with the given {@code key}; never {@code null} - */ - public List get(String key) { - final Path entryDir = dir.resolve(key); - if (Files.exists(entryDir)) { - try (Stream files = Files.list(entryDir)) { - return files - .filter(Files::isRegularFile) - .map(f -> f.getFileName().toString()) - .collect(Collectors.toList()); - - } catch (IOException e) { - throw new RuntimeException("Could not list " + entryDir, e); - } - } - return Collections.emptyList(); - } - - /** - * Add the given {@code value} to the collection associated with the given {@code key}. - * - * @param key - * @param value - */ - public void put(String key, String value) { - final Path entryDir = dir.resolve(key); - FsMap.safeCreateDirectories(entryDir); - final Path itemPath = entryDir.resolve(value); - try { - Files.write(itemPath, value.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException("Could not write to " + itemPath, e); - } - } - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/JavaDocParser.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/JavaDocParser.java deleted file mode 100644 index 4141db4ff6cd28..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/JavaDocParser.java +++ /dev/null @@ -1,546 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.hyphenate; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Node; -import org.jsoup.nodes.TextNode; - -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.javadoc.Javadoc; -import com.github.javaparser.javadoc.JavadocBlockTag; -import com.github.javaparser.javadoc.JavadocBlockTag.Type; -import com.github.javaparser.javadoc.description.JavadocDescription; -import com.github.javaparser.javadoc.description.JavadocDescriptionElement; -import com.github.javaparser.javadoc.description.JavadocInlineTag; - -import io.quarkus.annotation.processor.Constants; - -final class JavaDocParser { - - private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); - private static final Pattern REPLACE_WINDOWS_EOL = Pattern.compile("\r\n"); - private static final Pattern REPLACE_MACOS_EOL = Pattern.compile("\r"); - private static final Pattern STARTING_SPACE = Pattern.compile("^ +"); - - private static final String BACKTICK = "`"; - private static final String HASH = "#"; - private static final String STAR = "*"; - private static final String S_NODE = "s"; - private static final String UNDERSCORE = "_"; - private static final String NEW_LINE = "\n"; - private static final String LINK_NODE = "a"; - private static final String BOLD_NODE = "b"; - private static final String STRONG_NODE = "strong"; - private static final String BIG_NODE = "big"; - private static final String CODE_NODE = "code"; - private static final String DEL_NODE = "del"; - private static final String ITALICS_NODE = "i"; - private static final String EMPHASIS_NODE = "em"; - private static final String TEXT_NODE = "#text"; - private static final String UNDERLINE_NODE = "u"; - private static final String NEW_LINE_NODE = "br"; - private static final String PARAGRAPH_NODE = "p"; - private static final String SMALL_NODE = "small"; - private static final String LIST_ITEM_NODE = "li"; - private static final String HREF_ATTRIBUTE = "href"; - private static final String STRIKE_NODE = "strike"; - private static final String SUB_SCRIPT_NODE = "sub"; - private static final String ORDERED_LIST_NODE = "ol"; - private static final String SUPER_SCRIPT_NODE = "sup"; - private static final String UN_ORDERED_LIST_NODE = "ul"; - private static final String PREFORMATED_NODE = "pre"; - private static final String BLOCKQUOTE_NODE = "blockquote"; - - private static final String BIG_ASCIDOC_STYLE = "[.big]"; - private static final String LINK_ATTRIBUTE_FORMAT = "[%s]"; - private static final String SUB_SCRIPT_ASCIDOC_STYLE = "~"; - private static final String SUPER_SCRIPT_ASCIDOC_STYLE = "^"; - private static final String SMALL_ASCIDOC_STYLE = "[.small]"; - private static final String ORDERED_LIST_ITEM_ASCIDOC_STYLE = " . "; - private static final String UNORDERED_LIST_ITEM_ASCIDOC_STYLE = " - "; - private static final String UNDERLINE_ASCIDOC_STYLE = "[.underline]"; - private static final String LINE_THROUGH_ASCIDOC_STYLE = "[.line-through]"; - private static final String HARD_LINE_BREAK_ASCIDOC_STYLE = " +\n"; - private static final String CODE_BLOCK_ASCIDOC_STYLE = "```"; - private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE = "[quote]\n____"; - private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END = "____"; - - private final boolean inlineMacroMode; - - public JavaDocParser(boolean inlineMacroMode) { - this.inlineMacroMode = inlineMacroMode; - } - - public JavaDocParser() { - this(false); - } - - public String parseConfigDescription(String javadocComment) { - final AtomicReference 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()) { - javadocTextConsumer.accept(Constants.EMPTY); - return; - } - - // the parser expects all the lines to start with "* " - // we add it as it has been previously removed - javadocComment = START_OF_LINE.matcher(javadocComment).replaceAll("* "); - Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment); - - if (isAsciidoc(javadoc)) { - javadocTextConsumer.accept(handleEolInAsciidoc(javadoc)); - } else { - javadocTextConsumer.accept(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) { - if (javadocComment == null || javadocComment.trim().isEmpty()) { - return new SectionHolder(Constants.EMPTY, Constants.EMPTY); - } - - // the parser expects all the lines to start with "* " - // we add it as it has been previously removed - javadocComment = START_OF_LINE.matcher(javadocComment).replaceAll("* "); - Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment); - - if (isAsciidoc(javadoc)) { - final String details = handleEolInAsciidoc(javadoc); - final int endOfTitleIndex = details.indexOf(Constants.DOT); - final String title = details.substring(0, endOfTitleIndex).replaceAll("^([^\\w])+", Constants.EMPTY).trim(); - return new SectionHolder(title, details); - } - - return generateConfigSection(javadoc, sectionLevel); - } - - private SectionHolder generateConfigSection(Javadoc javadoc, int sectionLevel) { - final String generatedAsciiDoc = htmlJavadocToAsciidoc(javadoc.getDescription()); - if (generatedAsciiDoc.isEmpty()) { - return new SectionHolder(Constants.EMPTY, Constants.EMPTY); - } - - final String beginSectionDetails = IntStream - .rangeClosed(0, Math.max(0, sectionLevel)) - .mapToObj(x -> "=").collect(Collectors.joining()) - + " "; - - final int endOfTitleIndex = generatedAsciiDoc.indexOf(Constants.DOT); - if (endOfTitleIndex == -1) { - return new SectionHolder(generatedAsciiDoc.trim(), beginSectionDetails + generatedAsciiDoc); - } else { - final String title = generatedAsciiDoc.substring(0, endOfTitleIndex).trim(); - final String introduction = generatedAsciiDoc.substring(endOfTitleIndex + 1).trim(); - final String details = beginSectionDetails + title + "\n\n" + introduction; - - return new SectionHolder(title, details.trim()); - } - } - - private String handleEolInAsciidoc(Javadoc javadoc) { - // it's Asciidoc, so we just pass through - // it also uses platform specific EOL, so we need to convert them back to \n - String asciidoc = javadoc.getDescription().toText(); - asciidoc = REPLACE_WINDOWS_EOL.matcher(asciidoc).replaceAll("\n"); - asciidoc = REPLACE_MACOS_EOL.matcher(asciidoc).replaceAll("\n"); - return asciidoc; - } - - private boolean isAsciidoc(Javadoc javadoc) { - for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { - if ("asciidoclet".equals(blockTag.getTagName())) { - return true; - } - } - return false; - } - - private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { - StringBuilder sb = new StringBuilder(); - - for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { - if (javadocDescriptionElement instanceof JavadocInlineTag) { - JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; - String content = inlineTag.getContent().trim(); - switch (inlineTag.getType()) { - case CODE: - case VALUE: - case LITERAL: - case SYSTEM_PROPERTY: - sb.append('`'); - appendEscapedAsciiDoc(sb, content); - sb.append('`'); - break; - case LINK: - case LINKPLAIN: - if (content.startsWith(HASH)) { - content = hyphenate(content.substring(1)); - } - sb.append('`'); - appendEscapedAsciiDoc(sb, content); - sb.append('`'); - break; - default: - sb.append(content); - break; - } - } else { - appendHtml(sb, Jsoup.parseBodyFragment(javadocDescriptionElement.toText())); - } - } - - return trim(sb); - } - - private void appendHtml(StringBuilder sb, Node node) { - for (Node childNode : node.childNodes()) { - switch (childNode.nodeName()) { - case PARAGRAPH_NODE: - newLine(sb); - newLine(sb); - appendHtml(sb, childNode); - break; - case PREFORMATED_NODE: - newLine(sb); - newLine(sb); - sb.append(CODE_BLOCK_ASCIDOC_STYLE); - newLine(sb); - for (Node grandChildNode : childNode.childNodes()) { - unescapeHtmlEntities(sb, grandChildNode.toString()); - } - newLineIfNeeded(sb); - sb.append(CODE_BLOCK_ASCIDOC_STYLE); - newLine(sb); - newLine(sb); - break; - case BLOCKQUOTE_NODE: - newLine(sb); - newLine(sb); - sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE); - newLine(sb); - appendHtml(sb, childNode); - newLineIfNeeded(sb); - sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END); - newLine(sb); - newLine(sb); - break; - case ORDERED_LIST_NODE: - case UN_ORDERED_LIST_NODE: - newLine(sb); - appendHtml(sb, childNode); - break; - case LIST_ITEM_NODE: - final String marker = childNode.parent().nodeName().equals(ORDERED_LIST_NODE) - ? ORDERED_LIST_ITEM_ASCIDOC_STYLE - : UNORDERED_LIST_ITEM_ASCIDOC_STYLE; - newLine(sb); - sb.append(marker); - appendHtml(sb, childNode); - break; - case LINK_NODE: - final String link = childNode.attr(HREF_ATTRIBUTE); - sb.append("link:"); - sb.append(link); - final StringBuilder caption = new StringBuilder(); - appendHtml(caption, childNode); - sb.append(String.format(LINK_ATTRIBUTE_FORMAT, trim(caption))); - break; - case CODE_NODE: - sb.append(BACKTICK); - appendHtml(sb, childNode); - sb.append(BACKTICK); - break; - case BOLD_NODE: - case STRONG_NODE: - sb.append(STAR); - appendHtml(sb, childNode); - sb.append(STAR); - break; - case EMPHASIS_NODE: - case ITALICS_NODE: - sb.append(UNDERSCORE); - appendHtml(sb, childNode); - sb.append(UNDERSCORE); - break; - case UNDERLINE_NODE: - sb.append(UNDERLINE_ASCIDOC_STYLE); - sb.append(HASH); - appendHtml(sb, childNode); - sb.append(HASH); - break; - case SMALL_NODE: - sb.append(SMALL_ASCIDOC_STYLE); - sb.append(HASH); - appendHtml(sb, childNode); - sb.append(HASH); - break; - case BIG_NODE: - sb.append(BIG_ASCIDOC_STYLE); - sb.append(HASH); - appendHtml(sb, childNode); - sb.append(HASH); - break; - case SUB_SCRIPT_NODE: - sb.append(SUB_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode); - sb.append(SUB_SCRIPT_ASCIDOC_STYLE); - break; - case SUPER_SCRIPT_NODE: - sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode); - sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); - break; - case DEL_NODE: - case S_NODE: - case STRIKE_NODE: - sb.append(LINE_THROUGH_ASCIDOC_STYLE); - sb.append(HASH); - appendHtml(sb, childNode); - sb.append(HASH); - break; - case NEW_LINE_NODE: - sb.append(HARD_LINE_BREAK_ASCIDOC_STYLE); - break; - case TEXT_NODE: - String text = ((TextNode) childNode).text(); - - if (text.isEmpty()) { - break; - } - - // Indenting the first line of a paragraph by one or more spaces makes the block literal - // Please see https://docs.asciidoctor.org/asciidoc/latest/verbatim/literal-blocks/ for more info - // This prevents literal blocks f.e. after
- final var startingSpaceMatcher = STARTING_SPACE.matcher(text); - if (sb.length() > 0 && '\n' == sb.charAt(sb.length() - 1) && startingSpaceMatcher.find()) { - text = startingSpaceMatcher.replaceFirst(""); - } - - appendEscapedAsciiDoc(sb, text); - break; - default: - appendHtml(sb, childNode); - break; - } - } - } - - /** - * Trim the content of the given {@link StringBuilder} holding also AsciiDoc had line break {@code " +\n"} - * for whitespace in addition to characters <= {@code ' '}. - * - * @param sb the {@link StringBuilder} to trim - * @return the trimmed content of the given {@link StringBuilder} - */ - static String trim(StringBuilder sb) { - int length = sb.length(); - int offset = 0; - while (offset < length) { - final char ch = sb.charAt(offset); - if (ch == ' ' - && offset + 2 < length - && sb.charAt(offset + 1) == '+' - && sb.charAt(offset + 2) == '\n') { - /* Space followed by + and newline is AsciiDoc hard break that we consider whitespace */ - offset += 3; - continue; - } else if (ch > ' ') { - /* Non-whitespace as defined by String.trim() */ - break; - } - offset++; - } - if (offset > 0) { - sb.delete(0, offset); - } - if (sb.length() > 0) { - offset = sb.length() - 1; - while (offset >= 0) { - final char ch = sb.charAt(offset); - if (ch == '\n' - && offset - 2 >= 0 - && sb.charAt(offset - 1) == '+' - && sb.charAt(offset - 2) == ' ') { - /* Space followed by + is AsciiDoc hard break that we consider whitespace */ - offset -= 3; - continue; - } else if (ch > ' ') { - /* Non-whitespace as defined by String.trim() */ - break; - } - offset--; - } - if (offset < sb.length() - 1) { - sb.setLength(offset + 1); - } - } - return sb.toString(); - } - - private static StringBuilder newLineIfNeeded(StringBuilder sb) { - trimText(sb, " \t\r\n"); - return sb.append(NEW_LINE); - } - - private static StringBuilder newLine(StringBuilder sb) { - /* Trim trailing spaces and tabs at the end of line */ - trimText(sb, " \t"); - return sb.append(NEW_LINE); - } - - private static StringBuilder trimText(StringBuilder sb, String charsToTrim) { - while (sb.length() > 0 && charsToTrim.indexOf(sb.charAt(sb.length() - 1)) >= 0) { - sb.setLength(sb.length() - 1); - } - return sb; - } - - private StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { - int i = 0; - /* trim leading whitespace */ - LOOP: while (i < text.length()) { - switch (text.charAt(i++)) { - case ' ': - case '\t': - case '\r': - case '\n': - break; - default: - i--; - break LOOP; - } - } - for (; i < text.length(); i++) { - final char ch = text.charAt(i); - switch (ch) { - case '&': - int start = ++i; - while (i < text.length() && text.charAt(i) != ';') { - i++; - } - if (i > start) { - final String abbrev = text.substring(start, i); - switch (abbrev) { - case "lt": - sb.append('<'); - break; - case "gt": - sb.append('>'); - break; - case "nbsp": - sb.append("{nbsp}"); - break; - case "amp": - sb.append('&'); - break; - default: - try { - int code = Integer.parseInt(abbrev); - sb.append((char) code); - } catch (NumberFormatException e) { - throw new RuntimeException( - "Could not parse HTML entity &" + abbrev + "; in\n\n" + text + "\n\n"); - } - break; - } - } - break; - case '\r': - if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { - /* Ignore \r followed by \n */ - } else { - /* A Mac single \r: replace by \n */ - sb.append('\n'); - } - break; - default: - sb.append(ch); - - } - } - return sb; - } - - private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { - boolean escaping = false; - for (int i = 0; i < text.length(); i++) { - final char ch = text.charAt(i); - switch (ch) { - case ']': - // don't escape closing square bracket in the attribute list of an inline element with passThrough - // https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#substitutions - if (inlineMacroMode) { - if (escaping) { - sb.append("++"); - escaping = false; - } - sb.append("]"); - break; - } - case '#': - case '*': - case '\\': - case '{': - case '}': - case '[': - case '|': - if (!escaping) { - sb.append("++"); - escaping = true; - } - sb.append(ch); - break; - case '+': - if (escaping) { - sb.append("++"); - escaping = false; - } - sb.append("{plus}"); - break; - default: - if (escaping) { - sb.append("++"); - escaping = false; - } - sb.append(ch); - } - } - if (escaping) { - sb.append("++"); - } - return sb; - } - - static class SectionHolder { - final String title; - final String details; - - public SectionHolder(String title, String details) { - this.title = title; - this.details = details; - } - } -} 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 deleted file mode 100644 index a5b1fcf98cc0e6..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/MavenConfigDocBuilder.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static io.quarkus.annotation.processor.Constants.EMPTY; -import static io.quarkus.annotation.processor.Constants.NEW_LINE; -import static io.quarkus.annotation.processor.Constants.SECTION_TITLE_L1; - -import java.util.ArrayList; -import java.util.List; - -import io.quarkus.annotation.processor.Constants; - -public final class MavenConfigDocBuilder extends ConfigDocBuilder { - - public MavenConfigDocBuilder() { - super(false); - } - - private final JavaDocParser javaDocParser = new JavaDocParser(); - - public void addTableTitle(String goalTitle) { - write(SECTION_TITLE_L1, goalTitle, NEW_LINE); - } - - public void addNewLine() { - write(NEW_LINE); - } - - public void addTableDescription(String goalDescription) { - write(NEW_LINE, javaDocParser.parseConfigDescription(goalDescription), NEW_LINE); - } - - public GoalParamsBuilder newGoalParamsBuilder() { - return new GoalParamsBuilder(javaDocParser); - } - - private static abstract class TableBuilder { - - protected final List configDocItems = new ArrayList<>(); - - /** - * Section name that is displayed in a table header - */ - abstract protected String getSectionName(); - - public List build() { - - // a summary table - final ConfigDocSection parameterSection = new ConfigDocSection(); - parameterSection.setShowSection(true); - parameterSection.setName(getSectionName()); - parameterSection.setSectionDetailsTitle(getSectionName()); - parameterSection.setOptional(false); - parameterSection.setConfigDocItems(List.copyOf(configDocItems)); - - // topConfigDocItem wraps the summary table - final ConfigDocItem topConfigDocItem = new ConfigDocItem(); - topConfigDocItem.setConfigDocSection(parameterSection); - - return List.of(topConfigDocItem); - } - - public boolean tableIsNotEmpty() { - return !configDocItems.isEmpty(); - } - } - - public static final class GoalParamsBuilder extends TableBuilder { - - private final JavaDocParser javaDocParser; - - private GoalParamsBuilder(JavaDocParser javaDocParser) { - this.javaDocParser = javaDocParser; - } - - public void addParam(String type, String name, String defaultValue, boolean required, String description) { - final ConfigDocKey configDocKey = new ConfigDocKey(); - configDocKey.setType(type); - configDocKey.setKey(name); - configDocKey.setAdditionalKeys(List.of(name)); - configDocKey.setConfigPhase(ConfigPhase.RUN_TIME); - configDocKey.setDefaultValue(defaultValue == null ? Constants.EMPTY : defaultValue); - javaDocParser.parseConfigDescription(description, configDocKey::setConfigDoc, configDocKey::setSince); - configDocKey.setEnvironmentVariable(DocGeneratorUtil.toEnvVarName(name)); - configDocKey.setOptional(!required); - final ConfigDocItem configDocItem = new ConfigDocItem(); - configDocItem.setConfigDocKey(configDocKey); - configDocItems.add(configDocItem); - } - - @Override - protected String getSectionName() { - return "Parameter"; - } - } - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ScannedConfigDocsItemHolder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ScannedConfigDocsItemHolder.java deleted file mode 100644 index cba9d2bed6cabf..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ScannedConfigDocsItemHolder.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -final class ScannedConfigDocsItemHolder { - private final Map> configGroupConfigItems; - private final Map> configRootConfigItems; - private final Map configRootClassToConfigRootInfo = new HashMap<>(); - - public ScannedConfigDocsItemHolder() { - this(new HashMap<>(), new HashMap<>()); - } - - public ScannedConfigDocsItemHolder(Map> configRootConfigItems, - Map> configGroupConfigItems) { - this.configRootConfigItems = configRootConfigItems; - this.configGroupConfigItems = configGroupConfigItems; - } - - public Map> getConfigGroupConfigItems() { - return configGroupConfigItems; - } - - public Map> getConfigRootConfigItems() { - return configRootConfigItems; - } - - public void addConfigGroupItems(String configGroupName, List configDocItems) { - configGroupConfigItems.put(configGroupName, configDocItems); - } - - public void addConfigRootItems(ConfigRootInfo configRoot, List configDocItems) { - configRootConfigItems.put(configRoot, configDocItems); - configRootClassToConfigRootInfo.put(configRoot.getClazz().getQualifiedName().toString(), configRoot); - } - - public List getConfigItemsByRootClassName(String configRootClassName) { - ConfigRootInfo configRootInfo = configRootClassToConfigRootInfo.get(configRootClassName); - if (configRootInfo == null) { - return null; - } - - return configRootConfigItems.get(configRootInfo); - } - - public boolean isEmpty() { - return configRootConfigItems.isEmpty(); - } - - @Override - public String toString() { - return "ScannedConfigDocsItemHolder{" + - ", configRootConfigItems=" + configRootConfigItems + - ", configGroupConfigItems=" + configGroupConfigItems + - '}'; - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java deleted file mode 100644 index ce604d33eb1de2..00000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java +++ /dev/null @@ -1,156 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static io.quarkus.annotation.processor.Constants.CONFIG_PHASE_LEGEND; -import static io.quarkus.annotation.processor.Constants.NEW_LINE; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.toEnvVarName; - -import java.io.IOException; -import java.io.Writer; -import java.util.List; - -import io.quarkus.annotation.processor.Constants; - -final class SummaryTableDocFormatter implements DocFormatter { - private static final String TWO_NEW_LINES = "\n\n"; - private static final String TABLE_CLOSING_TAG = "\n|==="; - public static final String SEARCHABLE_TABLE_CLASS = ".searchable"; // a css class indicating if a table is searchable - public static final String CONFIGURATION_TABLE_CLASS = ".configuration-reference"; - private static final String TABLE_ROW_FORMAT = "\n\na|%s [[%s]]`link:#%s[%s]`\n\n[.description]\n--\n%s\n--%s|%s %s\n|%s\n"; - private static final String SECTION_TITLE = "[[%s]]link:#%s[%s]"; - private static final String TABLE_HEADER_FORMAT = "[%s, cols=\"80,.^10,.^10\"]\n|==="; - private static final String TABLE_SECTION_ROW_FORMAT = "\n\nh|%s\n%s\nh|Type\nh|Default"; - private final boolean showEnvVars; - - private String anchorPrefix = ""; - - public SummaryTableDocFormatter(boolean showEnvVars) { - this.showEnvVars = showEnvVars; - } - - public SummaryTableDocFormatter() { - this(true); - } - - /** - * Generate configuration keys in table format with search engine activated or not. - * Useful when we want to optionally activate or deactivate search engine - */ - @Override - public void format(Writer writer, String initialAnchorPrefix, boolean activateSearch, - List configDocItems, boolean includeConfigPhaseLegend) - throws IOException { - if (includeConfigPhaseLegend) { - writer.append("[.configuration-legend]").append(CONFIG_PHASE_LEGEND).append(NEW_LINE); - } - String searchableClass = activateSearch ? SEARCHABLE_TABLE_CLASS : Constants.EMPTY; - String tableClasses = CONFIGURATION_TABLE_CLASS + searchableClass; - writer.append(String.format(TABLE_HEADER_FORMAT, tableClasses)); - anchorPrefix = initialAnchorPrefix; - - // make sure that section-less configs get a legend - if (configDocItems.isEmpty() || configDocItems.get(0).isConfigKey()) { - String anchor = anchorPrefix + getAnchor("configuration"); - writer.append(String.format(TABLE_SECTION_ROW_FORMAT, - String.format(SECTION_TITLE, anchor, anchor, "Configuration property"), - Constants.EMPTY)); - } - - for (ConfigDocItem configDocItem : configDocItems) { - if (configDocItem.isConfigSection() && configDocItem.getConfigDocSection().isShowSection() - && configDocItem.getConfigDocSection().getAnchorPrefix() != null) { - anchorPrefix = configDocItem.getConfigDocSection().getAnchorPrefix() + "_"; - } - configDocItem.accept(writer, this); - } - - writer.append(TABLE_CLOSING_TAG); // close table - } - - @Override - public void format(Writer writer, ConfigDocKey configDocKey) throws IOException { - String typeContent = ""; - if (configDocKey.hasAcceptedValues()) { - if (configDocKey.isEnum()) { - typeContent = DocGeneratorUtil.joinEnumValues(configDocKey.getAcceptedValues()); - } else { - typeContent = DocGeneratorUtil.joinAcceptedValues(configDocKey.getAcceptedValues()); - } - } else if (configDocKey.hasType()) { - typeContent = configDocKey.computeTypeSimpleName(); - final String javaDocLink = configDocKey.getJavaDocSiteLink(); - if (!javaDocLink.isEmpty()) { - typeContent = String.format("link:%s[%s]\n", javaDocLink, typeContent); - } - } - if (configDocKey.isList()) { - typeContent = "list of " + typeContent; - } - - String doc = configDocKey.getConfigDoc(); - - if (showEnvVars) { - // Convert a property name to an environment variable name and show it in the config description - final String envVarExample = String.format("ifdef::add-copy-button-to-env-var[]\n" + - "Environment variable: env_var_with_copy_button:+++%1$s+++[]\n" + - "endif::add-copy-button-to-env-var[]\n" + - "ifndef::add-copy-button-to-env-var[]\n" + - "Environment variable: `+++%1$s+++`\n" + - "endif::add-copy-button-to-env-var[]", toEnvVarName(configDocKey.getKey())); - if (configDocKey.getConfigDoc().isEmpty()) { - doc = envVarExample; - } else { - // Add 2 new lines in order to show the environment variable on next line - doc += TWO_NEW_LINES + envVarExample; - } - } - - final String typeDetail = DocGeneratorUtil.getTypeFormatInformationNote(configDocKey); - final String defaultValue = configDocKey.getDefaultValue(); - // this is not strictly true, because we can have a required value with a default value, but - // for documentation it will do - String required = configDocKey.isOptional() || !defaultValue.isEmpty() ? "" - : "required icon:exclamation-circle[title=Configuration property is required]"; - String key = configDocKey.getKey(); - String configKeyAnchor = getAnchor(key); - String anchor = anchorPrefix + configKeyAnchor; - - StringBuilder keys = new StringBuilder(); - keys.append( - String.format("%s [[%s]]`link:#%s[%s]`\n\n", configDocKey.getConfigPhase().getIllustration(), anchor, anchor, - key)); - for (String additionalKey : configDocKey.getAdditionalKeys()) { - if (!additionalKey.equals(key)) { - keys.append(String.format("`link:#%s[%s]`\n\n", anchor, additionalKey)); - } - } - - writer.append(String.format("\n\na|%s\n[.description]\n--\n%s\n--%s|%s %s\n|%s\n", - keys, - // make sure nobody inserts a table cell separator here - doc.replace("|", "\\|"), - // if ConfigDocKey is enum, cell style operator must support block elements - configDocKey.isEnum() ? " a" : Constants.EMPTY, - typeContent, typeDetail, - defaultValue.isEmpty() ? required - : String.format("`%s`", defaultValue.replace("|", "\\|") - .replace("`", "\\`")))); - } - - @Override - public void format(Writer writer, ConfigDocSection configDocSection) throws IOException { - if (configDocSection.isShowSection()) { - String anchor = anchorPrefix - + getAnchor(configDocSection.getName() + Constants.DASH + configDocSection.getSectionDetailsTitle()); - String sectionTitle = String.format(SECTION_TITLE, anchor, anchor, configDocSection.getSectionDetailsTitle()); - final String sectionRow = String.format(TABLE_SECTION_ROW_FORMAT, sectionTitle, - configDocSection.isOptional() ? "This configuration section is optional" : Constants.EMPTY); - - writer.append(sectionRow); - } - - for (ConfigDocItem configDocItem : configDocSection.getConfigDocItems()) { - configDocItem.accept(writer, this); - } - } - -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/AccessorGenerator.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/AccessorGenerator.java new file mode 100644 index 00000000000000..2eb1efd69e24e4 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/AccessorGenerator.java @@ -0,0 +1,194 @@ +package io.quarkus.annotation.processor.util; + +import static javax.lang.model.util.ElementFilter.constructorsIn; +import static javax.lang.model.util.ElementFilter.fieldsIn; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; + +import org.jboss.jdeparser.FormatPreferences; +import org.jboss.jdeparser.JAssignableExpr; +import org.jboss.jdeparser.JCall; +import org.jboss.jdeparser.JClassDef; +import org.jboss.jdeparser.JDeparser; +import org.jboss.jdeparser.JExprs; +import org.jboss.jdeparser.JFiler; +import org.jboss.jdeparser.JMethodDef; +import org.jboss.jdeparser.JMod; +import org.jboss.jdeparser.JSourceFile; +import org.jboss.jdeparser.JSources; +import org.jboss.jdeparser.JType; +import org.jboss.jdeparser.JTypes; + +public final class AccessorGenerator { + + private static final String QUARKUS_GENERATED = "io.quarkus.Generated"; + private static final String INSTANCE_SYM = "__instance"; + + private final ProcessingEnvironment processingEnv; + private final ElementUtil elementUtil; + private final Set generatedAccessors = new ConcurrentHashMap().keySet(Boolean.TRUE); + + AccessorGenerator(ProcessingEnvironment processingEnv, ElementUtil elementUtil) { + this.processingEnv = processingEnv; + this.elementUtil = elementUtil; + } + + public void generateAccessor(final TypeElement clazz) { + if (!generatedAccessors.add(clazz.getQualifiedName().toString())) { + return; + } + final FormatPreferences fp = new FormatPreferences(); + final JSources sources = JDeparser.createSources(JFiler.newInstance(processingEnv.getFiler()), fp); + final PackageElement packageElement = elementUtil.getPackageOf(clazz); + final String className = elementUtil.buildRelativeBinaryName(clazz, new StringBuilder()).append("$$accessor") + .toString(); + final JSourceFile sourceFile = sources.createSourceFile(packageElement.getQualifiedName() + .toString(), className); + JType clazzType = JTypes.typeOf(clazz.asType()); + if (clazz.asType() instanceof DeclaredType) { + DeclaredType declaredType = ((DeclaredType) clazz.asType()); + TypeMirror enclosingType = declaredType.getEnclosingType(); + if (enclosingType != null && enclosingType.getKind() == TypeKind.DECLARED + && clazz.getModifiers() + .contains(Modifier.STATIC)) { + // Ugly workaround for Eclipse APT and static nested types + clazzType = unnestStaticNestedType(declaredType); + } + } + final JClassDef classDef = sourceFile._class(JMod.PUBLIC | JMod.FINAL, className); + classDef.constructor(JMod.PRIVATE); // no construction + classDef.annotate(QUARKUS_GENERATED) + .value("Quarkus annotation processor"); + final JAssignableExpr instanceName = JExprs.name(INSTANCE_SYM); + boolean isEnclosingClassPublic = clazz.getModifiers() + .contains(Modifier.PUBLIC); + // iterate fields + boolean generationNeeded = false; + for (VariableElement field : fieldsIn(clazz.getEnclosedElements())) { + final Set mods = field.getModifiers(); + if (mods.contains(Modifier.PRIVATE) || mods.contains(Modifier.STATIC) || mods.contains(Modifier.FINAL)) { + // skip it + continue; + } + final TypeMirror fieldType = field.asType(); + if (mods.contains(Modifier.PUBLIC) && isEnclosingClassPublic) { + // we don't need to generate a method accessor when the following conditions are met: + // 1) the field is public + // 2) the enclosing class is public + // 3) the class type of the field is public + if (fieldType instanceof DeclaredType) { + final DeclaredType declaredType = (DeclaredType) fieldType; + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + if (typeElement.getModifiers() + .contains(Modifier.PUBLIC)) { + continue; + } + } else { + continue; + } + + } + generationNeeded = true; + + final JType realType = JTypes.typeOf(fieldType); + final JType publicType = fieldType instanceof PrimitiveType ? realType : JType.OBJECT; + + final String fieldName = field.getSimpleName() + .toString(); + final JMethodDef getter = classDef.method(JMod.PUBLIC | JMod.STATIC, publicType, "get_" + fieldName); + getter.annotate(SuppressWarnings.class) + .value("unchecked"); + getter.param(JType.OBJECT, INSTANCE_SYM); + getter.body() + ._return(instanceName.cast(clazzType) + .field(fieldName)); + final JMethodDef setter = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.VOID, "set_" + fieldName); + setter.annotate(SuppressWarnings.class) + .value("unchecked"); + setter.param(JType.OBJECT, INSTANCE_SYM); + setter.param(publicType, fieldName); + final JAssignableExpr fieldExpr = JExprs.name(fieldName); + setter.body() + .assign(instanceName.cast(clazzType) + .field(fieldName), + (publicType.equals(realType) ? fieldExpr : fieldExpr.cast(realType))); + } + + // we need to generate an accessor if the class isn't public + if (!isEnclosingClassPublic) { + for (ExecutableElement ctor : constructorsIn(clazz.getEnclosedElements())) { + if (ctor.getModifiers() + .contains(Modifier.PRIVATE)) { + // skip it + continue; + } + generationNeeded = true; + StringBuilder b = new StringBuilder(); + for (VariableElement parameter : ctor.getParameters()) { + b.append('_'); + b.append(parameter.asType() + .toString() + .replace('.', '_')); + } + String codedName = b.toString(); + final JMethodDef ctorMethod = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.OBJECT, "construct" + codedName); + final JCall ctorCall = clazzType._new(); + for (VariableElement parameter : ctor.getParameters()) { + final TypeMirror paramType = parameter.asType(); + final JType realType = JTypes.typeOf(paramType); + final JType publicType = paramType instanceof PrimitiveType ? realType : JType.OBJECT; + final String name = parameter.getSimpleName() + .toString(); + ctorMethod.param(publicType, name); + final JAssignableExpr nameExpr = JExprs.name(name); + ctorCall.arg(publicType.equals(realType) ? nameExpr : nameExpr.cast(realType)); + } + ctorMethod.body() + ._return(ctorCall); + } + } + + // if no constructor or field access is needed, don't generate anything + if (generationNeeded) { + try { + sources.writeSources(); + } catch (IOException e) { + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to generate source file: " + e, clazz); + } + } + } + + private JType unnestStaticNestedType(DeclaredType declaredType) { + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + + final String name = typeElement.getQualifiedName() + .toString(); + final JType rawType = JTypes.typeNamed(name); + final List typeArguments = declaredType.getTypeArguments(); + if (typeArguments.isEmpty()) { + return rawType; + } + JType[] args = new JType[typeArguments.size()]; + for (int i = 0; i < typeArguments.size(); i++) { + final TypeMirror argument = typeArguments.get(i); + args[i] = JTypes.typeOf(argument); + } + return rawType.typeArg(args); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/Config.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Config.java new file mode 100644 index 00000000000000..85643f62c24eba --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Config.java @@ -0,0 +1,28 @@ +package io.quarkus.annotation.processor.util; + +import io.quarkus.annotation.processor.documentation.config.model.Extension; + +public class Config { + + private final Extension extension; + private final boolean useConfigMapping; + private final boolean debug; + + public Config(Extension extension, boolean useConfigMapping, boolean debug) { + this.extension = extension; + this.useConfigMapping = useConfigMapping; + this.debug = debug; + } + + public Extension getExtension() { + return extension; + } + + public boolean useConfigMapping() { + return useConfigMapping; + } + + public boolean isDebug() { + return debug; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java new file mode 100644 index 00000000000000..86d07a9229ef13 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java @@ -0,0 +1,184 @@ +package io.quarkus.annotation.processor.util; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.Name; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.StandardLocation; + +public class ElementUtil { + + private static final Pattern REMOVE_LEADING_SPACE = Pattern.compile("^ ", Pattern.MULTILINE); + + private final ProcessingEnvironment processingEnv; + + ElementUtil(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + } + + public String getQualifiedName(TypeMirror type) { + switch (type.getKind()) { + case BOOLEAN: + return "boolean"; + case BYTE: + return "byte"; + case CHAR: + return "char"; + case DOUBLE: + return "double"; + case FLOAT: + return "float"; + case INT: + return "int"; + case LONG: + return "long"; + case SHORT: + return "short"; + case DECLARED: + return ((TypeElement) ((DeclaredType) type).asElement()).getQualifiedName().toString(); + default: + // note that it includes annotations, which is something we don't want + // thus why all this additional work above... + // this default should never be triggered AFAIK, it's there to be extra safe + return type.toString(); + } + } + + public String getBinaryName(TypeElement clazz) { + return processingEnv.getElementUtils().getBinaryName(clazz).toString(); + } + + public String getRelativeBinaryName(TypeElement typeElement) { + return buildRelativeBinaryName(typeElement, new StringBuilder()).toString(); + } + + StringBuilder buildRelativeBinaryName(TypeElement typeElement, StringBuilder builder) { + final Element enclosing = typeElement.getEnclosingElement(); + if (enclosing instanceof TypeElement) { + buildRelativeBinaryName((TypeElement) enclosing, builder); + builder.append('$'); + } + builder.append(typeElement.getSimpleName()); + return builder; + } + + public String simplifyGenericType(TypeMirror typeMirror) { + DeclaredType declaredType = ((DeclaredType) typeMirror); + List typeArguments = declaredType.getTypeArguments(); + String simpleName = declaredType.asElement().getSimpleName().toString(); + if (typeArguments.isEmpty()) { + return simpleName; + } else if (typeArguments.size() == 1) { + return String.format("%s<%s>", simpleName, simplifyGenericType(typeArguments.get(0))); + } else if (typeArguments.size() == 2) { + return String.format("%s<%s,%s>", simpleName, simplifyGenericType(typeArguments.get(0)), + simplifyGenericType(typeArguments.get(1))); + } + + return "unknown"; // we should not reach here + } + + public Map getAnnotations(Element element) { + return element.getAnnotationMirrors().stream() + .collect(Collectors.toMap(a -> ((TypeElement) a.getAnnotationType().asElement()).getQualifiedName().toString(), + Function.identity())); + } + + public Map getAnnotationValues(AnnotationMirror annotation) { + return annotation.getElementValues().entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().toString().substring(0, e.getKey().toString().length() - 2), + e -> e.getValue().getValue())); + } + + public PackageElement getPackageOf(TypeElement clazz) { + return processingEnv.getElementUtils().getPackageOf(clazz); + } + + public Name getPackageName(TypeElement clazz) { + return getPackageOf(clazz).getQualifiedName(); + } + + public TypeElement getClassOf(Element e) { + Element t = e; + while (!(t instanceof TypeElement)) { + if (t == null) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + "Element " + e + " has no enclosing class"); + return null; + } + t = t.getEnclosingElement(); + } + return (TypeElement) t; + } + + public boolean isAnnotationPresent(Element element, String... annotationNames) { + Set annotations = Set.of(annotationNames); + for (AnnotationMirror i : element.getAnnotationMirrors()) { + String annotationName = ((TypeElement) i.getAnnotationType() + .asElement()).getQualifiedName() + .toString(); + if (annotations.contains(annotationName)) { + return true; + } + } + return false; + } + + /** + * This is less than ideal but it's the only way I found to detect if a class is local or not. + *

+ * It is important because, while we can scan the annotations of classes in the classpath, we cannot get their javadoc, + * which in the case of config doc generation is problematic. + */ + public boolean isLocalClass(TypeElement clazz) { + try { + while (clazz.getNestingKind().isNested()) { + clazz = (TypeElement) clazz.getEnclosingElement(); + } + + processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, "", + clazz.getQualifiedName().toString().replace('.', '/') + ".java"); + return true; + } catch (Exception e) { + return false; + } + } + + public Optional getJavadoc(Element e) { + String docComment = processingEnv.getElementUtils().getDocComment(e); + + if (docComment == null || docComment.isBlank()) { + return Optional.empty(); + } + + // javax.lang.model keeps the leading space after the "*" so we need to remove it. + + return Optional.of(REMOVE_LEADING_SPACE.matcher(docComment) + .replaceAll("") + .trim()); + } + + public void addMissingJavadocError(Element e) { + String error = "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e; + + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error, e); + throw new IllegalStateException(error); + } + + public boolean isJdkClass(TypeElement e) { + return e.getQualifiedName().toString().startsWith("java."); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ExtensionUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ExtensionUtil.java new file mode 100644 index 00000000000000..6634c70469e59b --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ExtensionUtil.java @@ -0,0 +1,171 @@ +package io.quarkus.annotation.processor.util; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.Diagnostic.Kind; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.annotation.processor.documentation.config.model.Extension.NameSource; + +public final class ExtensionUtil { + + private static final String ARTIFACT_DEPLOYMENT_SUFFIX = "-deployment"; + private static final String ARTIFACT_COMMON_SUFFIX = "-common"; + private static final String ARTIFACT_INTERNAL_SUFFIX = "-internal"; + private static final String NAME_QUARKUS_PREFIX = "Quarkus - "; + private static final String NAME_RUNTIME_SUFFIX = " - Runtime"; + private static final String NAME_DEPLOYMENT_SUFFIX = " - Deployment"; + private static final String NAME_COMMON_SUFFIX = " - Common"; + private static final String NAME_INTERNAL_SUFFIX = " - Internal"; + + private final ProcessingEnvironment processingEnv; + private final FilerUtil filerUtil; + + ExtensionUtil(ProcessingEnvironment processingEnv, FilerUtil filerUtil) { + this.processingEnv = processingEnv; + this.filerUtil = filerUtil; + } + + /** + * This is not exactly pretty but it's actually not easy to get the artifact id of the current artifact. + * One option would be to pass it through the annotation processor but it's not exactly ideal. + */ + public Extension getExtension() { + Optional pom = filerUtil.getPomPath(); + + if (pom.isEmpty()) { + return Extension.createNotDetected(); + } + + Document doc; + + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + DocumentBuilder db = dbf.newDocumentBuilder(); + doc = db.parse(pom.get().toFile()); + doc.getDocumentElement().normalize(); + } catch (Exception e) { + throw new IllegalStateException("Unable to parse pom file: " + pom, e); + } + + return getExtensionFromPom(pom.get(), doc); + } + + private Extension getExtensionFromPom(Path pom, Document doc) { + String parentGroupId = null; + String artifactId = null; + String groupId = null; + String name = null; + + NodeList children = doc.getDocumentElement().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (groupId != null && artifactId != null && name != null) { + break; + } + + Node child = children.item(i); + + if ("parent".equals(child.getNodeName())) { + NodeList parentChildren = child.getChildNodes(); + for (int j = 0; j < parentChildren.getLength(); j++) { + Node parentChild = parentChildren.item(j); + if ("groupId".equals(parentChild.getNodeName())) { + parentGroupId = parentChild.getTextContent() != null ? parentChild.getTextContent().trim() : null; + ; + break; + } + } + continue; + } + if ("groupId".equals(child.getNodeName())) { + groupId = child.getTextContent() != null ? child.getTextContent().trim() : null; + continue; + } + if ("artifactId".equals(child.getNodeName())) { + artifactId = child.getTextContent() != null ? child.getTextContent().trim() : null; + continue; + } + if ("name".equals(child.getNodeName())) { + name = child.getTextContent() != null ? child.getTextContent().trim() : null; + continue; + } + } + + if (groupId == null) { + groupId = parentGroupId; + } + + if (groupId == null || groupId.isBlank() || artifactId == null || artifactId.isBlank()) { + processingEnv.getMessager().printMessage(Kind.WARNING, "Unable to determine artifact coordinates from: " + pom); + return Extension.createNotDetected(); + } + + boolean commonOrInternal = false; + + if (artifactId.endsWith(ARTIFACT_DEPLOYMENT_SUFFIX)) { + artifactId = artifactId.substring(0, artifactId.length() - ARTIFACT_DEPLOYMENT_SUFFIX.length()); + } + if (artifactId.endsWith(ARTIFACT_COMMON_SUFFIX)) { + artifactId = artifactId.substring(0, artifactId.length() - ARTIFACT_COMMON_SUFFIX.length()); + commonOrInternal = true; + } + if (artifactId.endsWith(ARTIFACT_INTERNAL_SUFFIX)) { + artifactId = artifactId.substring(0, artifactId.length() - ARTIFACT_INTERNAL_SUFFIX.length()); + commonOrInternal = true; + } + + NameSource nameSource; + Optional nameFromExtensionMetadata = getExtensionNameFromExtensionMetadata(); + if (nameFromExtensionMetadata.isPresent()) { + name = nameFromExtensionMetadata.get(); + nameSource = commonOrInternal ? NameSource.EXTENSION_METADATA_COMMON_INTERNAL : NameSource.EXTENSION_METADATA; + } else if (name != null) { + nameSource = commonOrInternal ? NameSource.POM_XML_COMMON_INTERNAL : NameSource.POM_XML; + } else { + nameSource = NameSource.NONE; + } + + if (name != null) { + if (name.startsWith(NAME_QUARKUS_PREFIX)) { + name = name.substring(NAME_QUARKUS_PREFIX.length()).trim(); + } + if (name.endsWith(NAME_DEPLOYMENT_SUFFIX)) { + name = name.substring(0, name.length() - NAME_DEPLOYMENT_SUFFIX.length()); + } else if (name.endsWith(NAME_RUNTIME_SUFFIX)) { + name = name.substring(0, name.length() - NAME_RUNTIME_SUFFIX.length()); + } else if (name.endsWith(NAME_COMMON_SUFFIX)) { + name = name.substring(0, name.length() - NAME_COMMON_SUFFIX.length()); + } else if (name.endsWith(NAME_INTERNAL_SUFFIX)) { + name = name.substring(0, name.length() - NAME_INTERNAL_SUFFIX.length()); + } + } + + return new Extension(groupId, artifactId, name, nameSource, true); + } + + private Optional getExtensionNameFromExtensionMetadata() { + Optional> extensionMetadata = filerUtil.getExtensionMetadata(); + + if (extensionMetadata.isEmpty()) { + return Optional.empty(); + } + + String extensionName = (String) extensionMetadata.get().get("name"); + if (extensionName == null || extensionName.isBlank()) { + return Optional.empty(); + } + + return Optional.of(extensionName.trim()); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/FilerUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/FilerUtil.java new file mode 100644 index 00000000000000..068606fe933b34 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/FilerUtil.java @@ -0,0 +1,195 @@ +package io.quarkus.annotation.processor.util; + +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.Diagnostic; +import javax.tools.Diagnostic.Kind; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; +import io.quarkus.bootstrap.util.PropertyUtils; + +public class FilerUtil { + + private final ProcessingEnvironment processingEnv; + + FilerUtil(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + } + + /** + * This method uses the annotation processor Filer API and we shouldn't use a Path as paths containing \ are not supported. + */ + public void write(String filePath, Set set) { + if (set.isEmpty()) { + return; + } + + try { + final FileObject listResource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", + filePath.toString()); + + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(listResource.openOutputStream(), StandardCharsets.UTF_8))) { + for (String className : set) { + writer.write(className); + writer.newLine(); + } + } + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write " + filePath + ": " + e); + return; + } + } + + /** + * This method uses the annotation processor Filer API and we shouldn't use a Path as paths containing \ are not supported. + */ + public void write(String filePath, Properties properties) { + if (properties.isEmpty()) { + return; + } + + try { + final FileObject propertiesResource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", + filePath.toString()); + + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(propertiesResource.openOutputStream(), StandardCharsets.UTF_8))) { + PropertyUtils.store(properties, writer); + } + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write " + filePath + ": " + e); + return; + } + } + + /** + * This method uses the annotation processor Filer API and we shouldn't use a Path as paths containing \ are not supported. + */ + public void writeJson(String filePath, Object value) { + if (value == null) { + return; + } + + try { + final FileObject jsonResource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", + filePath.toString()); + + try (OutputStream os = jsonResource.openOutputStream()) { + JacksonMappers.jsonObjectWriter().writeValue(os, value); + } + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write " + filePath + ": " + e); + return; + } + } + + /** + * This method uses the annotation processor Filer API and we shouldn't use a Path as paths containing \ are not supported. + */ + public void writeYaml(String filePath, Object value) { + if (value == null) { + return; + } + + try { + final FileObject yamlResource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", + filePath.toString()); + + try (OutputStream os = yamlResource.openOutputStream()) { + JacksonMappers.yamlObjectWriter().writeValue(os, value); + } + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write " + filePath + ": " + e); + return; + } + } + + /** + * The model files are written outside of target/classes as we don't want to include them in the jar. + *

+ * They are not written by the annotation processor Filer API so we can use proper Paths. + */ + public Path writeModel(String filePath, Object value) { + Path yamlModelPath = getTargetPath().resolve(filePath); + try { + Files.createDirectories(yamlModelPath.getParent()); + JacksonMappers.yamlObjectWriter().writeValue(yamlModelPath.toFile(), value); + + return yamlModelPath; + } catch (IOException e) { + throw new IllegalStateException("Unable to write the model to: " + yamlModelPath, e); + } + } + + public Path getTargetPath() { + try { + FileObject dummyFile = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", "dummy"); + return Paths.get(dummyFile.toUri()).getParent().getParent(); + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to determine the path of target/" + e); + throw new UncheckedIOException(e); + } + } + + public Optional getPomPath() { + try { + Path pomPath = Paths.get(processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", "dummy").toUri()) + .getParent().getParent().getParent().resolve("pom.xml"); + + if (!Files.isReadable(pomPath)) { + return Optional.empty(); + } + + return Optional.of(pomPath.toAbsolutePath()); + } catch (IOException e) { + return Optional.empty(); + } + } + + public Optional> getExtensionMetadata() { + String extensionMetadataDescriptor = "META-INF/quarkus-extension.yaml"; + + try { + FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", + extensionMetadataDescriptor); + if (fileObject == null) { + return Optional.empty(); + } + + try (InputStream is = fileObject.openInputStream()) { + String yamlMetadata = new String(is.readAllBytes(), StandardCharsets.UTF_8); + Map extensionMetadata = JacksonMappers.yamlObjectReader().readValue(yamlMetadata, Map.class); + + return Optional.of(extensionMetadata); + } + } catch (NoSuchFileException | FileNotFoundException e) { + // ignore + // we could get the URI, create a Path and check that the path exists but it seems a bit overkill + return Optional.empty(); + } catch (IOException e) { + processingEnv.getMessager().printMessage(Kind.WARNING, + "Unable to read extension metadata file: " + extensionMetadataDescriptor + " because of " + + e.getClass().getName() + ": " + e.getMessage()); + return Optional.empty(); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/Strings.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Strings.java new file mode 100644 index 00000000000000..2976bad169b741 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Strings.java @@ -0,0 +1,15 @@ +package io.quarkus.annotation.processor.util; + +public final class Strings { + + private Strings() { + } + + public static boolean isBlank(String string) { + return string == null || string.isBlank(); + } + + public static boolean isEmpty(String string) { + return string == null || string.isEmpty(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/Utils.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Utils.java new file mode 100644 index 00000000000000..66b3c2b161b075 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/Utils.java @@ -0,0 +1,40 @@ +package io.quarkus.annotation.processor.util; + +import javax.annotation.processing.ProcessingEnvironment; + +public final class Utils { + + private final ProcessingEnvironment processingEnv; + private final ElementUtil elementUtil; + private final AccessorGenerator accessorGenerator; + private final FilerUtil filerUtil; + private final ExtensionUtil extensionUtil; + + public Utils(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + this.elementUtil = new ElementUtil(processingEnv); + this.accessorGenerator = new AccessorGenerator(processingEnv, elementUtil); + this.filerUtil = new FilerUtil(processingEnv); + this.extensionUtil = new ExtensionUtil(processingEnv, filerUtil); + } + + public ElementUtil element() { + return elementUtil; + } + + public ProcessingEnvironment processingEnv() { + return processingEnv; + } + + public AccessorGenerator accessorGenerator() { + return accessorGenerator; + } + + public FilerUtil filer() { + return filerUtil; + } + + public ExtensionUtil extension() { + return extensionUtil; + } +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java index d9d85a19ab53a0..df5122c68afd89 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java @@ -9,7 +9,7 @@ import javax.tools.JavaFileObject; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,29 +18,22 @@ import com.karuslabs.elementary.junit.annotations.Classpath; import com.karuslabs.elementary.junit.annotations.Processors; -import io.quarkus.annotation.processor.fs.CustomMemoryFileSystemProvider; - @ExtendWith(JavacExtension.class) @Processors({ ExtensionAnnotationProcessor.class }) +@Disabled class ExtensionAnnotationProcessorTest { - @BeforeEach - void beforeEach() { - // This is of limited use, since the filesystem doesn't seem to directly generate files, in the current usage - CustomMemoryFileSystemProvider.reset(); - } - @Test @Classpath("org.acme.examples.ClassWithBuildStep") void shouldProcessClassWithBuildStepWithoutErrors(Results results) throws IOException { - assertNoErrrors(results); + assertNoErrors(results); } @Test @Classpath("org.acme.examples.ClassWithBuildStep") void shouldGenerateABscFile(Results results) throws IOException { - assertNoErrrors(results); - List sources = results.sources; + assertNoErrors(results); + List sources = results.generatedSources; JavaFileObject bscFile = sources.stream() .filter(source -> source.getName() .endsWith(".bsc")) @@ -62,10 +55,10 @@ private String removeLineBreaks(String s) { @Test @Classpath("org.acme.examples.ClassWithoutBuildStep") void shouldProcessEmptyClassWithoutErrors(Results results) { - assertNoErrrors(results); + assertNoErrors(results); } - private static void assertNoErrrors(Results results) { + private static void assertNoErrors(Results results) { assertEquals(0, results.find() .errors() .count(), diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java new file mode 100644 index 00000000000000..546e8cc05b11f9 --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java @@ -0,0 +1,382 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Collections; + +import org.asciidoctor.Asciidoctor.Factory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; + +public class JavadocToAsciidocTransformerConfigItemTest { + + @Test + public void removeParagraphIndentation() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("First paragraph

Second Paragraph"); + assertEquals("First paragraph +\n +\nSecond Paragraph", + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + } + + @Test + public void parseUntrimmedJavaDoc() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(" "); + assertNull(parsed.description()); + + parsed = JavadocUtil.parseConfigItemJavadoc("

"); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertNull(description); + } + + @Test + public void parseJavaDocWithParagraph() { + String javaDoc = "hello

world

"; + String expectedOutput = "hello\n\nworld"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + + javaDoc = "hello world

bonjour

le monde

"; + expectedOutput = "hello world\n\nbonjour\n\nle monde"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithStyles() { + // Bold + String javaDoc = "hello world"; + String expectedOutput = "hello *world*"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + javaDoc = "hello world"; + expectedOutput = "hello *world*"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + // Emphasized + javaDoc = "hello world"; + expectedOutput = "_hello world_"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + // Italics + javaDoc = "hello world"; + expectedOutput = "_hello world_"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + // Underline + javaDoc = "hello world"; + expectedOutput = "[.underline]#hello world#"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + // small + javaDoc = "quarkus subatomic"; + expectedOutput = "[.small]#quarkus subatomic#"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + // big + javaDoc = "hello world"; + expectedOutput = "[.big]#hello world#"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + // line through + javaDoc = "hello monolith world"; + expectedOutput = "[.line-through]#hello #[.line-through]#monolith #[.line-through]#world#"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + + // superscript and subscript + javaDoc = "cloud in-premise"; + expectedOutput = "^cloud ^~in-premise~"; + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithLiTagsInsideUlTag() { + String javaDoc = "List:" + + "
    \n" + + "
  • 1
  • \n" + + "
  • 2
  • \n" + + "
" + + ""; + String expectedOutput = "List:\n\n - 1\n - 2"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithLiTagsInsideOlTag() { + String javaDoc = "List:" + + "
    \n" + + "
  1. 1
  2. \n" + + "
  3. 2
  4. \n" + + "
" + + ""; + String expectedOutput = "List:\n\n . 1\n . 2"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithLinkInlineSnippet() { + String javaDoc = "{@link firstlink} {@link #secondlink} \n {@linkplain #third.link}"; + String expectedOutput = "`firstlink` `secondlink` `third.link`"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithLinkTag() { + String javaDoc = "this is a hello link"; + String expectedOutput = "this is a link:http://link.com[hello] link"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithCodeInlineSnippet() { + String javaDoc = "{@code true} {@code false}"; + String expectedOutput = "`true` `false`"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithLiteralInlineSnippet() { + String javaDoc = "{@literal java.util.Boolean}"; + String expectedOutput = "`java.util.Boolean`"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithValueInlineSnippet() { + String javaDoc = "{@value 10s}"; + String expectedOutput = "`10s`"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithUnknownInlineSnippet() { + String javaDoc = "{@see java.util.Boolean}"; + String expectedOutput = "java.util.Boolean"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithUnknownNode() { + String javaDoc = "hello"; + String expectedOutput = "hello"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + + assertEquals(expectedOutput, description); + } + + @Test + public void parseJavaDocWithBlockquoteBlock() { + ParsedJavadoc parsed = JavadocUtil + .parseConfigItemJavadoc("See Section 4.5.5 of the JSR 380 specification, specifically\n" + + "\n" + + "
\n" + + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may\n" + + "be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation.\n" + + "This would pose a strengthening of preconditions to be fulfilled by the caller.\n" + + "
\nThat was interesting, wasn't it?"); + + assertEquals("See Section 4.5.5 of the JSR 380 specification, specifically\n" + + "\n" + + "[quote]\n" + + "____\n" + + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation. This would pose a strengthening of preconditions to be fulfilled by the caller.\n" + + "____\n" + + "\n" + + "That was interesting, wasn't it?", + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + parsed = JavadocUtil.parseConfigItemJavadoc( + "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz"); + + assertEquals( + "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + // TODO + // assertEquals("Example:\n\n```\nfoo\nbar\n```", + // JavadocUtil.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); + } + + @Test + public void parseJavaDocWithCodeBlock() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("Example:\n\n
\nfoo\nbar\n
\n\nbaz"); + + assertEquals("Example:\n\n```\nfoo\nbar\n```\n\nbaz", + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + parsed = JavadocUtil.parseConfigItemJavadoc( + "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz"); + + assertEquals( + "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + // TODO + // assertEquals("Example:\n\n```\nfoo\nbar\n```", + // JavadocUtil.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); + } + + @Test + public void asciidoc() { + String asciidoc = "== My Asciidoc\n" + + "\n" + + "Let's have a https://quarkus.io[link to our website].\n" + + "\n" + + "[TIP]\n" + + "====\n" + + "A nice tip\n" + + "====\n" + + "\n" + + "[source,java]\n" + + "----\n" + + "And some code\n" + + "----"; + + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet"); + + assertEquals(asciidoc, JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + } + + @Test + public void asciidocLists() { + String asciidoc = "* A list\n" + + "\n" + + "* 1\n" + + " * 1.1\n" + + " * 1.2\n" + + "* 2"; + + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet"); + + assertEquals(asciidoc, JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + } + + @ParameterizedTest + @ValueSource(strings = { "#", "*", "\\", "[", "]", "|" }) + public void escape(String ch) { + final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; + + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); + final String expected = "
\n

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

\n
"; + assertEquals(expected, actual); + } + + @ParameterizedTest + @ValueSource(strings = { "#", "*", "\\", "[", "]", "|" }) + public void escapeInsideInlineElement(String ch) { + final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; + + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format(), true); + final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); + + if (ch.equals("]")) { + ch = "]"; + } + final String expected = "
\n

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

\n
"; + assertEquals(expected, actual); + } + + @Test + public void escapePlus() { + final String javaDoc = "Inline + ++, HTML tag glob + ++, {@code JavaDoc tag + ++}"; + final String expected = "
\n

Inline + ++, HTML tag glob + ++, JavaDoc tag + ++

\n
"; + + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); + assertEquals(expected, actual); + } + + @ParameterizedTest + @ValueSource(strings = { "{", "}" }) + public void escapeBrackets(String ch) { + final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + + ""; + final String expected = "
\n

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

\n
"; + + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); + assertEquals(expected, actual); + } + + @Test + void trim() { + assertEquals("+ \nfoo", JavadocToAsciidocTransformer.trim(new StringBuilder("+ \nfoo"))); + assertEquals("+", JavadocToAsciidocTransformer.trim(new StringBuilder(" +"))); + assertEquals("foo", JavadocToAsciidocTransformer.trim(new StringBuilder(" +\nfoo"))); + assertEquals("foo +", JavadocToAsciidocTransformer.trim(new StringBuilder("foo +"))); + assertEquals("foo", JavadocToAsciidocTransformer.trim(new StringBuilder("foo"))); + assertEquals("+", JavadocToAsciidocTransformer.trim(new StringBuilder("+ \n"))); + assertEquals("+", JavadocToAsciidocTransformer.trim(new StringBuilder(" +\n+ \n"))); + assertEquals("", JavadocToAsciidocTransformer.trim(new StringBuilder(" +\n"))); + assertEquals("foo", JavadocToAsciidocTransformer.trim(new StringBuilder(" \n\tfoo"))); + assertEquals("foo", JavadocToAsciidocTransformer.trim(new StringBuilder("foo \n\t"))); + assertEquals("foo", JavadocToAsciidocTransformer.trim(new StringBuilder(" \n\tfoo \n\t"))); + assertEquals("", JavadocToAsciidocTransformer.trim(new StringBuilder(""))); + assertEquals("", JavadocToAsciidocTransformer.trim(new StringBuilder(" \n\t"))); + assertEquals("+", JavadocToAsciidocTransformer.trim(new StringBuilder(" +"))); + assertEquals("", JavadocToAsciidocTransformer.trim(new StringBuilder(" +\n"))); + assertEquals("", JavadocToAsciidocTransformer.trim(new StringBuilder(" +\n +\n"))); + assertEquals("foo +\nbar", JavadocToAsciidocTransformer.trim(new StringBuilder(" foo +\nbar +\n"))); + } + +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java new file mode 100644 index 00000000000000..9ab4290efa591b --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java @@ -0,0 +1,61 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; + +public class JavadocToAsciidocTransformerConfigSectionTest { + + @Test + public void parseUntrimmedJavaDoc() { + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc(" "); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.title(), parsed.format())); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.details(), parsed.format())); + + parsed = JavadocUtil.parseConfigSectionJavadoc("

"); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.title(), parsed.format())); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.details(), parsed.format())); + } + + @Test + public void passThroughAConfigSectionInAsciiDoc() { + String title = "My Asciidoc"; + String details = "Let's have a https://quarkus.io[link to our website].\n" + + "\n" + + "[TIP]\n" + + "====\n" + + "A nice tip\n" + + "====\n" + + "\n" + + "[source,java]\n" + + "----\n" + + "And some code\n" + + "----"; + + String asciidoc = "=== " + title + "\n\n" + details; + + ParsedJavadocSection sectionHolder = JavadocUtil.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); + assertEquals(title, JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.title(), sectionHolder.format())); + assertEquals(details, JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.details(), sectionHolder.format())); + + asciidoc = "Asciidoc title. \n" + + "\n" + + "Let's have a https://quarkus.io[link to our website].\n" + + "\n" + + "[TIP]\n" + + "====\n" + + "A nice tip\n" + + "====\n" + + "\n" + + "[source,java]\n" + + "----\n" + + "And some code\n" + + "----"; + + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); + assertEquals("Asciidoc title", JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.title(), sectionHolder.format())); + } +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/ConfigNamingUtilTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/ConfigNamingUtilTest.java new file mode 100644 index 00000000000000..ae98968392ea65 --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/ConfigNamingUtilTest.java @@ -0,0 +1,106 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.annotation.processor.documentation.config.model.ConfigPhase; + +public class ConfigNamingUtilTest { + + @Test + public void replaceNonAlphanumericByUnderscoresThenConvertToUpperCase() { + assertEquals("QUARKUS_DATASOURCE__DATASOURCE_NAME__JDBC_BACKGROUND_VALIDATION_INTERVAL", + ConfigNamingUtil.toEnvVarName("quarkus.datasource.\"datasource-name\".jdbc.background-validation-interval")); + assertEquals( + "QUARKUS_SECURITY_JDBC_PRINCIPAL_QUERY__NAMED_PRINCIPAL_QUERIES__BCRYPT_PASSWORD_MAPPER_ITERATION_COUNT_INDEX", + ConfigNamingUtil.toEnvVarName( + "quarkus.security.jdbc.principal-query.\"named-principal-queries\".bcrypt-password-mapper.iteration-count-index")); + } + + @Test + void getRootPrefixTest() { + String prefix = "quarkus"; + String name = Markers.HYPHENATED_ELEMENT_NAME; + String simpleClassName = "MyConfig"; + String actual = ConfigNamingUtil.getRootPrefix(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("quarkus.my", actual); + + prefix = "my.prefix"; + name = ""; + simpleClassName = "MyPrefix"; + actual = ConfigNamingUtil.getRootPrefix(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("my.prefix", actual); + + prefix = ""; + name = "my.prefix"; + simpleClassName = "MyPrefix"; + actual = ConfigNamingUtil.getRootPrefix(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("my.prefix", actual); + + prefix = "my"; + name = "prefix"; + simpleClassName = "MyPrefix"; + actual = ConfigNamingUtil.getRootPrefix(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("my.prefix", actual); + + prefix = "quarkus"; + name = "prefix"; + simpleClassName = "SomethingElse"; + actual = ConfigNamingUtil.getRootPrefix(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("quarkus.prefix", actual); + + prefix = ""; + name = Markers.HYPHENATED_ELEMENT_NAME; + simpleClassName = "SomethingElse"; + actual = ConfigNamingUtil.getRootPrefix(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("quarkus.something-else", actual); + } + + @Test + public void derivingConfigRootNameTestCase() { + // should hyphenate class name + String simpleClassName = "RootName"; + String actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + // should hyphenate class name after removing Config(uration) suffix + simpleClassName = "RootNameConfig"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_TIME); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameConfiguration"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_AND_RUN_TIME_FIXED); + assertEquals("quarkus.root-name", actual); + + // should hyphenate class name after removing RunTimeConfig(uration) suffix + simpleClassName = "RootNameRunTimeConfig"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameRuntimeConfig"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameRunTimeConfiguration"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + // should hyphenate class name after removing BuildTimeConfig(uration) suffix + simpleClassName = "RootNameBuildTimeConfig"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_AND_RUN_TIME_FIXED); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameBuildTimeConfiguration"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_TIME); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootName"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "prefix", ConfigPhase.RUN_TIME); + assertEquals("prefix.root-name", actual); + + simpleClassName = "RootName"; + actual = ConfigNamingUtil.deriveConfigRootName(simpleClassName, "my.prefix", ConfigPhase.RUN_TIME); + assertEquals("my.prefix.root-name", actual); + } +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java new file mode 100644 index 00000000000000..1b16410465b84f --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java @@ -0,0 +1,231 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; + +public class JavadocUtilTest { + + @Test + public void parseNullJavaDoc() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(null); + assertNull(parsed.description()); + } + + @Test + public void parseSimpleJavaDoc() { + String javaDoc = "hello world"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + + assertEquals(javaDoc, parsed.description()); + } + + @Test + public void since() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("Javadoc text\n\n@since 1.2.3"); + assertEquals("Javadoc text", parsed.description()); + assertEquals("1.2.3", parsed.since()); + } + + @Test + public void deprecated() { + ParsedJavadoc parsed = JavadocUtil + .parseConfigItemJavadoc("@deprecated JNI is always enabled starting from GraalVM 19.3.1."); + assertEquals(null, parsed.description()); + } + + @Test + public void parseNullSection() { + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc(null); + assertEquals(null, parsed.details()); + assertEquals(null, parsed.title()); + } + + @Test + public void parseSimpleSection() { + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc("title"); + assertEquals("title", parsed.title()); + assertEquals(null, parsed.details()); + } + + @Test + public void parseSectionWithIntroduction() { + /** + * Simple javadoc + */ + String javaDoc = "Config Section .Introduction"; + String expectedDetails = "Introduction"; + String expectedTitle = "Config Section"; + assertEquals(expectedTitle, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + + /** + * html javadoc + */ + javaDoc = "

Config Section

. Introduction"; + expectedDetails = "Introduction"; + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + assertEquals(expectedTitle, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void parseSectionWithParagraph() { + String javaDoc = "Dev Services\n

\nDev Services allows Quarkus to automatically start Elasticsearch in dev and test mode."; + assertEquals("Dev Services", JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void properlyParseConfigSectionWrittenInHtml() { + String javaDoc = "Config Section.

This is section introduction"; + String expectedDetails = "

This is section introduction"; + String title = "Config Section"; + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + assertEquals(title, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void parseSectionWithoutIntroduction() { + /** + * Simple javadoc + */ + String javaDoc = "Config Section"; + String expectedTitle = "Config Section"; + String expectedDetails = null; + ParsedJavadocSection sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + + javaDoc = "Config Section."; + expectedTitle = "Config Section"; + expectedDetails = null; + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + + /** + * html javadoc + */ + javaDoc = "

Config Section

"; + expectedTitle = "Config Section"; + expectedDetails = null; + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + } + + @Test + public void shouldReturnEmptyListForPrimitiveValue() { + String value = JavadocUtil.getJavadocSiteLink("int"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink("long"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink("float"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink("boolean"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink("double"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink("char"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink("short"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink("byte"); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Boolean.class.getName()); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Byte.class.getName()); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Short.class.getName()); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Integer.class.getName()); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Long.class.getName()); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Float.class.getName()); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Double.class.getName()); + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(Character.class.getName()); + assertNull(value); + } + + @Test + public void shouldReturnALinkToOfficialJavaDocIfIsJavaOfficialType() { + String value = JavadocUtil.getJavadocSiteLink(String.class.getName()); + // for String, we don't return a Javadoc link as it's a very basic type + assertNull(value); + + value = JavadocUtil.getJavadocSiteLink(InetAddress.class.getName()); + assertEquals(JavadocUtil.OFFICIAL_JAVA_DOC_BASE_LINK + "java/net/InetAddress.html", value); + + value = JavadocUtil.getJavadocSiteLink(BigInteger.class.getName()); + assertEquals(JavadocUtil.OFFICIAL_JAVA_DOC_BASE_LINK + "java/math/BigInteger.html", value); + + value = JavadocUtil.getJavadocSiteLink(Duration.class.getName()); + assertEquals(JavadocUtil.OFFICIAL_JAVA_DOC_BASE_LINK + "java/time/Duration.html", value); + + value = JavadocUtil.getJavadocSiteLink((Map.Entry.class.getName().replace('$', '.'))); + assertEquals(JavadocUtil.OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/Map.Entry.html", value); + + value = JavadocUtil.getJavadocSiteLink(Map.Entry.class.getName()); + assertEquals(JavadocUtil.OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/Map.Entry.html", value); + + value = JavadocUtil.getJavadocSiteLink(List.class.getName()); + assertEquals(JavadocUtil.OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/List.html", value); + + value = JavadocUtil.getJavadocSiteLink("java.util.List"); + assertEquals(JavadocUtil.OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/List.html", value); + } + + @Test + public void shouldReturnALinkToAgroalJavaDocIfTypeIsDeclaredInAgroalPackage() { + String value = JavadocUtil.getJavadocSiteLink( + "io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation"); + assertEquals(JavadocUtil.AGROAL_API_JAVA_DOC_SITE + + "io/agroal/api/configuration/AgroalConnectionFactoryConfiguration.TransactionIsolation.html", value); + + value = JavadocUtil.getJavadocSiteLink("io.agroal.api.AgroalDataSource.FlushMode"); + assertEquals(JavadocUtil.AGROAL_API_JAVA_DOC_SITE + "io/agroal/api/AgroalDataSource.FlushMode.html", value); + } + + @Test + public void shouldReturnALinkToVertxJavaDocIfTypeIsDeclaredInVertxPackage() { + String value = JavadocUtil.getJavadocSiteLink( + "io.vertx.core.Context"); + assertEquals(JavadocUtil.VERTX_JAVA_DOC_SITE + "io/vertx/core/Context.html", value); + + value = JavadocUtil.getJavadocSiteLink("io.vertx.amqp.AmqpMessage"); + assertEquals(JavadocUtil.VERTX_JAVA_DOC_SITE + "io/vertx/amqp/AmqpMessage.html", value); + } + + @Test + public void shouldReturnEmptyLinkIfUnknownJavaDocType() { + String value = JavadocUtil.getJavadocSiteLink("io.quarkus.ConfigDocKey"); + assertNull(value); + } +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/TypeUtilTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/TypeUtilTest.java new file mode 100644 index 00000000000000..e25c5d652087f9 --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/TypeUtilTest.java @@ -0,0 +1,20 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TypeUtilTest { + + @Test + public void normalizeDurationValueTest() { + assertEquals("", TypeUtil.normalizeDurationValue("")); + assertEquals("1S", TypeUtil.normalizeDurationValue("1")); + assertEquals("1S", TypeUtil.normalizeDurationValue("1S")); + assertEquals("1S", TypeUtil.normalizeDurationValue("1s")); + + // values are not validated here + assertEquals("1_000", TypeUtil.normalizeDurationValue("1_000")); + assertEquals("FOO", TypeUtil.normalizeDurationValue("foo")); + } +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java deleted file mode 100644 index 432bd86b334e9d..00000000000000 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java +++ /dev/null @@ -1,158 +0,0 @@ -package io.quarkus.annotation.processor.fs; - -import java.io.IOException; -import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.FileStore; -import java.nio.file.FileSystem; -import java.nio.file.Path; -import java.nio.file.PathMatcher; -import java.nio.file.Paths; -import java.nio.file.WatchService; -import java.nio.file.attribute.UserPrincipalLookupService; -import java.nio.file.spi.FileSystemProvider; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class CustomMemoryFileSystem extends FileSystem { - - private final Map fileContents = new HashMap<>(); - private final CustomMemoryFileSystemProvider provider; - - public CustomMemoryFileSystem(CustomMemoryFileSystemProvider provider) { - this.provider = provider; - } - - @Override - public FileSystemProvider provider() { - return provider; - } - - @Override - public void close() throws IOException { - // No resources to close - } - - @Override - public boolean isOpen() { - return true; // Always open - } - - @Override - public boolean isReadOnly() { - return false; // This filesystem is writable - } - - @Override - public String getSeparator() { - return "/"; // Unix-style separator - } - - @Override - public Iterable getRootDirectories() { - return Collections.singleton(Paths.get("/")); // Single root directory - } - - @Override - public Iterable getFileStores() { - return Collections.emptyList(); // No file stores - } - - @Override - public Set supportedFileAttributeViews() { - return Collections.emptySet(); // No supported file attribute views - } - - @Override - public Path getPath(String first, String... more) { - String path = first; - for (String segment : more) { - path += "/" + segment; - } - return Paths.get(path); - } - - @Override - public PathMatcher getPathMatcher(String syntaxAndPattern) { - return null; - } - - @Override - public UserPrincipalLookupService getUserPrincipalLookupService() { - return null; - } - - @Override - public WatchService newWatchService() throws IOException { - return null; - } - - public void addFile(URI uri, byte[] content) { - fileContents.put(uri, ByteBuffer.wrap(content)); - } - - static class CustomMemorySeekableByteChannel implements SeekableByteChannel { - - private final ByteBuffer buffer; - - CustomMemorySeekableByteChannel(ByteBuffer buffer) { - this.buffer = buffer; - } - - @Override - public int read(ByteBuffer dst) throws IOException { - int remaining = buffer.remaining(); - int count = Math.min(remaining, dst.remaining()); - if (count > 0) { - ByteBuffer slice = buffer.slice(); - slice.limit(count); - dst.put(slice); - buffer.position(buffer.position() + count); - } - return count; - } - - @Override - public int write(ByteBuffer src) throws IOException { - int count = src.remaining(); - buffer.put(src); - return count; - } - - @Override - public long position() throws IOException { - return buffer.position(); - } - - @Override - public SeekableByteChannel position(long newPosition) throws IOException { - buffer.position((int) newPosition); - return this; - } - - @Override - public long size() throws IOException { - return buffer.limit(); - } - - @Override - public SeekableByteChannel truncate(long size) throws IOException { - buffer.limit((int) size); - return this; - } - - @Override - public boolean isOpen() { - return true; // Always open - } - - @Override - public void close() throws IOException { - // No resources to close - } - } - -} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java deleted file mode 100644 index 8d28a7ae672a63..00000000000000 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java +++ /dev/null @@ -1,152 +0,0 @@ -package io.quarkus.annotation.processor.fs; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.AccessMode; -import java.nio.file.CopyOption; -import java.nio.file.DirectoryStream; -import java.nio.file.FileStore; -import java.nio.file.FileSystem; -import java.nio.file.LinkOption; -import java.nio.file.NoSuchFileException; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.spi.FileSystemProvider; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class CustomMemoryFileSystemProvider extends FileSystemProvider { - - private static final String MEM = "mem"; - - private static Map fileContents = new HashMap(); - - public static void reset() { - fileContents = new HashMap(); - } - - public static Set getCreatedFiles() { - return fileContents.keySet(); - } - - @Override - public String getScheme() { - return MEM; - } - - @Override - public FileSystem newFileSystem(URI uri, Map env) throws IOException { - // There's a bit of a disconnect here between the Elementary JavaFileManager and the memory filesystem, - // even though both are in-memory filesystems - return new CustomMemoryFileSystem(this); - } - - @Override - public FileSystem getFileSystem(URI uri) { - throw new UnsupportedOperationException(); - } - - @Override - public Path getPath(URI uri) { - - if (uri.getScheme() == null || !uri.getScheme() - .equalsIgnoreCase(MEM)) { - throw new IllegalArgumentException("For URI " + uri + ", URI scheme is not '" + MEM + "'"); - - } - - // TODO what should we do here? Can we use the java file manager used by Elementary? - try { - return Path.of(File.createTempFile("mem-fs", "adhoc") - .toURI()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) - throws IOException { - if (fileContents.containsKey(path.toUri())) { - ByteBuffer buffer = fileContents.get(path.toUri()); - return new CustomMemoryFileSystem.CustomMemorySeekableByteChannel(buffer); - } else { - throw new NoSuchFileException(path.toString()); - } - } - - @Override - public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void delete(Path path) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void copy(Path source, Path target, CopyOption... options) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void move(Path source, Path target, CopyOption... options) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isSameFile(Path path1, Path path2) throws IOException { - return path1.equals(path2); - } - - @Override - public boolean isHidden(Path path) throws IOException { - return false; - } - - @Override - public FileStore getFileStore(Path path) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void checkAccess(Path path, AccessMode... modes) throws IOException { - if (!fileContents.containsKey(path.toUri())) { - throw new NoSuchFileException(path.toString()); - } - } - - @Override - public V getFileAttributeView(Path path, Class type, - LinkOption... options) { - throw new UnsupportedOperationException(); - } - - @Override - public A readAttributes(Path path, Class type, LinkOption... options) - throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { - throw new UnsupportedOperationException(); - } -} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKeyTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKeyTest.java deleted file mode 100644 index 8a5c4b0ba8324a..00000000000000 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKeyTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.time.Duration; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class ConfigDocKeyTest { - - private ConfigDocKey configDocKey; - - @BeforeEach - public void setup() { - configDocKey = new ConfigDocKey(); - } - - @Test - public void shouldComputePrimitiveSimpleName() { - configDocKey.setType(int.class.getSimpleName()); - String simpleName = configDocKey.computeTypeSimpleName(); - assertEquals(int.class.getSimpleName(), simpleName); - - configDocKey.setType(long.class.getSimpleName()); - simpleName = configDocKey.computeTypeSimpleName(); - assertEquals(long.class.getSimpleName(), simpleName); - - configDocKey.setType(boolean.class.getSimpleName()); - simpleName = configDocKey.computeTypeSimpleName(); - assertEquals(boolean.class.getSimpleName(), simpleName); - } - - @Test - public void shouldComputeClassSimpleName() { - configDocKey.setType(Duration.class.getName()); - String simpleName = configDocKey.computeTypeSimpleName(); - assertEquals(Duration.class.getSimpleName(), simpleName); - - configDocKey.setType(List.class.getName()); - simpleName = configDocKey.computeTypeSimpleName(); - assertEquals(List.class.getSimpleName(), simpleName); - - configDocKey.setType(String.class.getName()); - simpleName = configDocKey.computeTypeSimpleName(); - assertEquals(String.class.getSimpleName(), simpleName); - - configDocKey.setType(Map.Entry.class.getName()); - simpleName = configDocKey.computeTypeSimpleName(); - assertEquals(Map.Entry.class.getSimpleName(), simpleName); - } -} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtilTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtilTest.java deleted file mode 100644 index 0cf6ca266f7261..00000000000000 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtilTest.java +++ /dev/null @@ -1,429 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.AGROAL_API_JAVA_DOC_SITE; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.OFFICIAL_JAVA_DOC_BASE_LINK; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.VERTX_JAVA_DOC_SITE; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.appendConfigItemsIntoExistingOnes; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigGroupDocFileName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigRootDocFileName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeExtensionDocFileName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.deriveConfigRootName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.getJavaDocSiteLink; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.getName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.normalizeDurationValue; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.toEnvVarName; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.math.BigInteger; -import java.net.InetAddress; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import io.quarkus.annotation.processor.Constants; - -public class DocGeneratorUtilTest { - @Test - public void shouldReturnEmptyListForPrimitiveValue() { - String value = getJavaDocSiteLink("int"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink("long"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink("float"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink("boolean"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink("double"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink("char"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink("short"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink("byte"); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Boolean.class.getName()); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Byte.class.getName()); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Short.class.getName()); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Integer.class.getName()); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Long.class.getName()); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Float.class.getName()); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Double.class.getName()); - assertEquals(Constants.EMPTY, value); - - value = getJavaDocSiteLink(Character.class.getName()); - assertEquals(Constants.EMPTY, value); - } - - @Test - public void shouldReturnALinkToOfficialJavaDocIfIsJavaOfficialType() { - String value = getJavaDocSiteLink(String.class.getName()); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/lang/String.html", value); - - value = getJavaDocSiteLink(InetAddress.class.getName()); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/net/InetAddress.html", value); - - value = getJavaDocSiteLink(BigInteger.class.getName()); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/math/BigInteger.html", value); - - value = getJavaDocSiteLink(Duration.class.getName()); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/time/Duration.html", value); - - value = getJavaDocSiteLink((Map.Entry.class.getName().replace('$', '.'))); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/Map.Entry.html", value); - - value = getJavaDocSiteLink(Map.Entry.class.getName()); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/Map.Entry.html", value); - - value = getJavaDocSiteLink(List.class.getName()); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/List.html", value); - - value = getJavaDocSiteLink("java.util.List"); - assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/List.html", value); - } - - @Test - public void replaceNonAlphanumericByUnderscoresThenConvertToUpperCase() { - assertEquals("QUARKUS_DATASOURCE__DATASOURCE_NAME__JDBC_BACKGROUND_VALIDATION_INTERVAL", - toEnvVarName("quarkus.datasource.\"datasource-name\".jdbc.background-validation-interval")); - assertEquals( - "QUARKUS_SECURITY_JDBC_PRINCIPAL_QUERY__NAMED_PRINCIPAL_QUERIES__BCRYPT_PASSWORD_MAPPER_ITERATION_COUNT_INDEX", - toEnvVarName( - "quarkus.security.jdbc.principal-query.\"named-principal-queries\".bcrypt-password-mapper.iteration-count-index")); - } - - @Test - public void shouldReturnALinkToAgroalJavaDocIfTypeIsDeclaredInAgroalPackage() { - String value = getJavaDocSiteLink( - "io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation"); - assertEquals(AGROAL_API_JAVA_DOC_SITE - + "io/agroal/api/configuration/AgroalConnectionFactoryConfiguration.TransactionIsolation.html", value); - - value = getJavaDocSiteLink("io.agroal.api.AgroalDataSource.FlushMode"); - assertEquals(AGROAL_API_JAVA_DOC_SITE + "io/agroal/api/AgroalDataSource.FlushMode.html", value); - } - - @Test - public void shouldReturnALinkToVertxJavaDocIfTypeIsDeclaredInVertxPackage() { - String value = getJavaDocSiteLink( - "io.vertx.core.Context"); - assertEquals(VERTX_JAVA_DOC_SITE + "io/vertx/core/Context.html", value); - - value = getJavaDocSiteLink("io.vertx.amqp.AmqpMessage"); - assertEquals(VERTX_JAVA_DOC_SITE + "io/vertx/amqp/AmqpMessage.html", value); - } - - @Test - public void shouldReturnEmptyLinkIfUnknownJavaDocType() { - String value = getJavaDocSiteLink("io.quarkus.ConfigDocKey"); - assertEquals(Constants.EMPTY, value); - } - - @Test - public void shouldReturnConfigRootNameWhenComputingExtensionName() { - String configRoot = "org.acme.ConfigRoot"; - String expected = "org.acme.ConfigRoot.adoc"; - String fileName = computeExtensionDocFileName(configRoot); - assertEquals(expected, fileName); - } - - @Test - public void shouldUseCoreForConfigRootsCoreModuleWhenComputingExtensionName() { - String configRoot = "io.quarkus.runtime.RuntimeConfig"; - String expected = "quarkus-core.adoc"; - String fileName = computeExtensionDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.deployment.BuildTimeConfig"; - expected = "quarkus-core.adoc"; - fileName = computeExtensionDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.deployment.path.BuildTimeConfig"; - expected = "quarkus-core.adoc"; - fileName = computeExtensionDocFileName(configRoot); - assertEquals(expected, fileName); - } - - @Test - public void shouldGuessArtifactIdWhenComputingExtensionName() { - String configRoot = "io.quarkus.agroal.Config"; - String expected = "quarkus-agroal.adoc"; - String fileName = computeExtensionDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.keycloak.Config"; - expected = "quarkus-keycloak.adoc"; - fileName = computeExtensionDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.extension.name.BuildTimeConfig"; - expected = "quarkus-extension-name.adoc"; - fileName = computeExtensionDocFileName(configRoot); - assertEquals(expected, fileName); - } - - @Test - public void shouldUseHyphenatedClassNameWithoutRuntimeOrDeploymentNamespaceWhenComputingConfigGroupFileName() { - String configRoot = "ClassName"; - String expected = "config-group-class-name.adoc"; - String fileName = computeConfigGroupDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.agroal.ConfigGroup"; - expected = "quarkus-agroal-config-group.adoc"; - fileName = computeConfigGroupDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.agroal.runtime.ClassName"; - expected = "quarkus-agroal-config-group-class-name.adoc"; - fileName = computeConfigGroupDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.keycloak.deployment.RealmConfig"; - expected = "quarkus-keycloak-config-group-realm-config.adoc"; - fileName = computeConfigGroupDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.extension.deployment.BuildTimeConfig"; - expected = "quarkus-extension-config-group-build-time-config.adoc"; - fileName = computeConfigGroupDocFileName(configRoot); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.extension.deployment.name.BuildTimeConfig"; - expected = "quarkus-extension-config-group-name-build-time-config.adoc"; - fileName = computeConfigGroupDocFileName(configRoot); - assertEquals(expected, fileName); - } - - @Test - public void shouldUseHyphenatedClassNameWithEverythingBeforeRuntimeOrDeploymentNamespaceReplacedByConfigRootNameWhenComputingConfigRootFileName() { - String configRoot = "ClassName"; - String expected = "root-name-class-name.adoc"; - String fileName = computeConfigRootDocFileName(configRoot, "root-name"); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.agroal.runtime.ClassName"; - expected = "quarkus-datasource-class-name.adoc"; - fileName = computeConfigRootDocFileName(configRoot, "quarkus-datasource"); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.keycloak.deployment.RealmConfig"; - expected = "quarkus-keycloak-realm-config.adoc"; - fileName = computeConfigRootDocFileName(configRoot, "quarkus-keycloak"); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.extension.deployment.BuildTimeConfig"; - expected = "quarkus-root-10-build-time-config.adoc"; - fileName = computeConfigRootDocFileName(configRoot, "quarkus-root-10"); - assertEquals(expected, fileName); - - configRoot = "io.quarkus.extension.deployment.name.BuildTimeConfig"; - expected = "quarkus-config-root-name-build-time-config.adoc"; - fileName = computeConfigRootDocFileName(configRoot, "quarkus-config-root"); - assertEquals(expected, fileName); - } - - @Test - public void shouldPreserveExistingConfigItemsWhenAppendAnEmptyConfigItems() { - List existingConfigItems = Arrays.asList(new ConfigDocItem(), new ConfigDocItem()); - appendConfigItemsIntoExistingOnes(existingConfigItems, Collections.emptyList()); - assertEquals(2, existingConfigItems.size()); - } - - @Test - public void shouldAppendNewConfigItemsAtTheEndOfExistingConfigItems() { - List existingConfigItems = new ArrayList<>( - Arrays.asList(new ConfigDocItem(null, new ConfigDocKey()), new ConfigDocItem(null, new ConfigDocKey()))); - ConfigDocItem newItem = new ConfigDocItem(null, new ConfigDocKey()); - ConfigDocSection configDocSection = new ConfigDocSection(); - configDocSection.setSectionDetailsTitle("title"); - ConfigDocItem section = new ConfigDocItem(configDocSection, null); - List newConfigItems = Arrays.asList(newItem, section); - - appendConfigItemsIntoExistingOnes(existingConfigItems, newConfigItems); - - assertEquals(4, existingConfigItems.size()); - List addedList = existingConfigItems.subList(2, 4); - assertEquals(newItem, addedList.get(0)); - assertEquals(section, addedList.get(1)); - } - - @Test - public void shouldAppendConfigSectionConfigItemsIntoExistingConfigItemsOfConfigSectionWithSameTitle() { - ConfigDocSection existingSection = new ConfigDocSection(); - existingSection.setSectionDetailsTitle("title"); - ConfigDocItem configItem = new ConfigDocItem(null, new ConfigDocKey()); - existingSection.addConfigDocItems(Arrays.asList(configItem)); - - ConfigDocItem configDocItem = new ConfigDocItem(existingSection, null); - List existingConfigItems = new ArrayList<>(Arrays.asList(configDocItem)); - - ConfigDocSection configDocSection = new ConfigDocSection(); - configDocSection.setSectionDetailsTitle("title"); - ConfigDocItem newConfigItem = new ConfigDocItem(null, new ConfigDocKey()); - configDocSection.addConfigDocItems(Arrays.asList(newConfigItem)); - ConfigDocItem section = new ConfigDocItem(configDocSection, null); - - appendConfigItemsIntoExistingOnes(existingConfigItems, Arrays.asList(section)); - - assertEquals(1, existingConfigItems.size()); - assertEquals(2, existingSection.getConfigDocItems().size()); - - assertEquals(configItem, existingSection.getConfigDocItems().get(0)); - assertEquals(newConfigItem, existingSection.getConfigDocItems().get(1)); - } - - // TODO - should deep merge be supported? Or we should only merge top level sections? - @Test - public void shouldDeepAppendConfigSectionConfigItemsIntoExistingConfigItemsOfConfigSectionWithSameTitle() { - ConfigDocSection deepSection = new ConfigDocSection(); - deepSection.setSectionDetailsTitle("title"); - ConfigDocItem deepConfigKey = new ConfigDocItem(null, new ConfigDocKey()); - deepSection.addConfigDocItems(Arrays.asList(deepConfigKey)); - ConfigDocItem deepConfigItem = new ConfigDocItem(deepSection, null); - - ConfigDocSection section = new ConfigDocSection(); - section.setSectionDetailsTitle(""); - section.addConfigDocItems(Arrays.asList(deepConfigItem)); - - ConfigDocItem configItemWithDeepSection = new ConfigDocItem(section, null); - List existingConfigItems = new ArrayList<>(Arrays.asList(configItemWithDeepSection)); - - ConfigDocSection configDocSection = new ConfigDocSection(); - configDocSection.setSectionDetailsTitle("title"); - ConfigDocItem configItem = new ConfigDocItem(null, new ConfigDocKey()); - configDocSection.addConfigDocItems(Arrays.asList(configItem)); - ConfigDocItem configDocItem = new ConfigDocItem(configDocSection, null); - - appendConfigItemsIntoExistingOnes(existingConfigItems, Arrays.asList(configDocItem)); - - assertEquals(1, existingConfigItems.size()); - assertEquals(2, deepSection.getConfigDocItems().size()); - - assertEquals(deepConfigKey, deepSection.getConfigDocItems().get(0)); - assertEquals(configItem, deepSection.getConfigDocItems().get(1)); - } - - @Test - void getNameTest() { - String prefix = Constants.QUARKUS; - String name = Constants.HYPHENATED_ELEMENT_NAME; - String simpleClassName = "MyConfig"; - String actual = getName(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); - assertEquals("quarkus.my", actual); - - prefix = "my.prefix"; - name = ""; - simpleClassName = "MyPrefix"; - actual = getName(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); - assertEquals("my.prefix", actual); - - prefix = ""; - name = "my.prefix"; - simpleClassName = "MyPrefix"; - actual = getName(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); - assertEquals("my.prefix", actual); - - prefix = "my"; - name = "prefix"; - simpleClassName = "MyPrefix"; - actual = getName(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); - assertEquals("my.prefix", actual); - - prefix = Constants.QUARKUS; - name = "prefix"; - simpleClassName = "SomethingElse"; - actual = getName(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); - assertEquals("quarkus.prefix", actual); - - prefix = ""; - name = Constants.HYPHENATED_ELEMENT_NAME; - simpleClassName = "SomethingElse"; - actual = getName(prefix, name, simpleClassName, ConfigPhase.RUN_TIME); - assertEquals("quarkus.something-else", actual); - } - - @Test - public void derivingConfigRootNameTestCase() { - // should hyphenate class name - String simpleClassName = "RootName"; - String actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); - assertEquals("quarkus.root-name", actual); - - // should hyphenate class name after removing Config(uration) suffix - simpleClassName = "RootNameConfig"; - actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_TIME); - assertEquals("quarkus.root-name", actual); - - simpleClassName = "RootNameConfiguration"; - actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_AND_RUN_TIME_FIXED); - assertEquals("quarkus.root-name", actual); - - // should hyphenate class name after removing RunTimeConfig(uration) suffix - simpleClassName = "RootNameRunTimeConfig"; - actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); - assertEquals("quarkus.root-name", actual); - - simpleClassName = "RootNameRuntimeConfig"; - actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); - assertEquals("quarkus.root-name", actual); - - simpleClassName = "RootNameRunTimeConfiguration"; - actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.RUN_TIME); - assertEquals("quarkus.root-name", actual); - - // should hyphenate class name after removing BuildTimeConfig(uration) suffix - simpleClassName = "RootNameBuildTimeConfig"; - actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_AND_RUN_TIME_FIXED); - assertEquals("quarkus.root-name", actual); - - simpleClassName = "RootNameBuildTimeConfiguration"; - actual = deriveConfigRootName(simpleClassName, "", ConfigPhase.BUILD_TIME); - assertEquals("quarkus.root-name", actual); - - simpleClassName = "RootName"; - actual = deriveConfigRootName(simpleClassName, "prefix", ConfigPhase.RUN_TIME); - assertEquals("prefix.root-name", actual); - - simpleClassName = "RootName"; - actual = deriveConfigRootName(simpleClassName, "my.prefix", ConfigPhase.RUN_TIME); - assertEquals("my.prefix.root-name", actual); - } - - @Test - public void normalizeDurationValueTest() { - assertEquals("", normalizeDurationValue("")); - assertEquals("1S", normalizeDurationValue("1")); - assertEquals("1S", normalizeDurationValue("1S")); - assertEquals("1S", normalizeDurationValue("1s")); - - // values are not validated here - assertEquals("1_000", normalizeDurationValue("1_000")); - assertEquals("FOO", normalizeDurationValue("foo")); - } -} 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 deleted file mode 100644 index 60d4200fe52a36..00000000000000 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigDescriptionParserTest.java +++ /dev/null @@ -1,370 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -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; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -public class JavaDocConfigDescriptionParserTest { - - private JavaDocParser parser; - - @BeforeEach - public void setup() { - parser = new JavaDocParser(); - } - - @Test - public void parseNullJavaDoc() { - String parsed = parser.parseConfigDescription(null); - assertEquals("", parsed); - } - - @Test - public void removeParagraphIndentation() { - String parsed = parser.parseConfigDescription("First paragraph

Second Paragraph"); - assertEquals("First paragraph +\n +\nSecond Paragraph", parsed); - } - - @Test - public void parseUntrimmedJavaDoc() { - String parsed = parser.parseConfigDescription(" "); - assertEquals("", parsed); - parsed = parser.parseConfigDescription("

"); - assertEquals("", parsed); - } - - @Test - public void parseSimpleJavaDoc() { - String javaDoc = "hello world"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(javaDoc, parsed); - } - - @Test - public void parseJavaDocWithParagraph() { - String javaDoc = "hello

world

"; - String expectedOutput = "hello\n\nworld"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - - javaDoc = "hello world

bonjour

le monde

"; - expectedOutput = "hello world\n\nbonjour\n\nle monde"; - parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithStyles() { - // Bold - String javaDoc = "hello world"; - String expectedOutput = "hello *world*"; - String parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - javaDoc = "hello world"; - expectedOutput = "hello *world*"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - // Emphasized - javaDoc = "hello world"; - expectedOutput = "_hello world_"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - // Italics - javaDoc = "hello world"; - expectedOutput = "_hello world_"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - // Underline - javaDoc = "hello world"; - expectedOutput = "[.underline]#hello world#"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - // small - javaDoc = "quarkus subatomic"; - expectedOutput = "[.small]#quarkus subatomic#"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - // big - javaDoc = "hello world"; - expectedOutput = "[.big]#hello world#"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - // line through - javaDoc = "hello monolith world"; - expectedOutput = "[.line-through]#hello #[.line-through]#monolith #[.line-through]#world#"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - - // superscript and subscript - javaDoc = "cloud in-premise"; - expectedOutput = "^cloud ^~in-premise~"; - parsed = parser.parseConfigDescription(javaDoc); - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithLiTagsInsideUlTag() { - String javaDoc = "List:" + - "
    \n" + - "
  • 1
  • \n" + - "
  • 2
  • \n" + - "
" + - ""; - String expectedOutput = "List:\n\n - 1\n - 2"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithLiTagsInsideOlTag() { - String javaDoc = "List:" + - "
    \n" + - "
  1. 1
  2. \n" + - "
  3. 2
  4. \n" + - "
" + - ""; - String expectedOutput = "List:\n\n . 1\n . 2"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithLinkInlineSnippet() { - String javaDoc = "{@link firstlink} {@link #secondlink} \n {@linkplain #third.link}"; - String expectedOutput = "`firstlink` `secondlink` `third.link`"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithLinkTag() { - String javaDoc = "this is a
hello link"; - String expectedOutput = "this is a link:http://link.com[hello] link"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithCodeInlineSnippet() { - String javaDoc = "{@code true} {@code false}"; - String expectedOutput = "`true` `false`"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithLiteralInlineSnippet() { - String javaDoc = "{@literal java.util.Boolean}"; - String expectedOutput = "`java.util.Boolean`"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithValueInlineSnippet() { - String javaDoc = "{@value 10s}"; - String expectedOutput = "`10s`"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithUnknownInlineSnippet() { - String javaDoc = "{@see java.util.Boolean}"; - String expectedOutput = "java.util.Boolean"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithUnknownNode() { - String javaDoc = "hello"; - String expectedOutput = "hello"; - String parsed = parser.parseConfigDescription(javaDoc); - - assertEquals(expectedOutput, parsed); - } - - @Test - public void parseJavaDocWithBlockquoteBlock() { - assertEquals("See Section 4.5.5 of the JSR 380 specification, specifically\n" - + "\n" - + "[quote]\n" - + "____\n" - + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation. This would pose a strengthening of preconditions to be fulfilled by the caller.\n" - + "____\n" - + "\n" - + "That was interesting, wasn't it?", - parser.parseConfigDescription("See Section 4.5.5 of the JSR 380 specification, specifically\n" - + "\n" - + "
\n" - + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may\n" - + "be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation.\n" - + "This would pose a strengthening of preconditions to be fulfilled by the caller.\n" - + "
\nThat was interesting, wasn't it?")); - - assertEquals( - "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", - parser.parseConfigDescription( - "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz")); - - // TODO - // assertEquals("Example:\n\n```\nfoo\nbar\n```", - // parser.parseConfigDescription("Example:\n\n
{@code\nfoo\nbar\n}
")); - } - - @Test - public void parseJavaDocWithCodeBlock() { - assertEquals("Example:\n\n```\nfoo\nbar\n```\n\nbaz", - parser.parseConfigDescription("Example:\n\n
\nfoo\nbar\n
\n\nbaz")); - - assertEquals( - "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", - parser.parseConfigDescription( - "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz")); - - // TODO - // assertEquals("Example:\n\n```\nfoo\nbar\n```", - // 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" + - "\n" + - "Let's have a https://quarkus.io[link to our website].\n" + - "\n" + - "[TIP]\n" + - "====\n" + - "A nice tip\n" + - "====\n" + - "\n" + - "[source,java]\n" + - "----\n" + - "And some code\n" + - "----"; - - assertEquals(asciidoc, parser.parseConfigDescription(asciidoc + "\n" + "@asciidoclet")); - } - - @Test - public void asciidocLists() { - String asciidoc = "* A list\n" + - "\n" + - "* 1\n" + - " * 1.1\n" + - " * 1.2\n" + - "* 2"; - - assertEquals(asciidoc, parser.parseConfigDescription(asciidoc + "\n" + "@asciidoclet")); - } - - @ParameterizedTest - @ValueSource(strings = { "#", "*", "\\", "[", "]", "|" }) - public void escape(String ch) { - final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch - + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; - - final String asciiDoc = parser.parseConfigDescription(javaDoc); - final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); - final String expected = "
\n

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

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

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

\n
"; - assertEquals(expected, actual); - } - - @Test - public void escapePlus() { - final String javaDoc = "Inline + ++, HTML tag glob + ++, {@code JavaDoc tag + ++}"; - final String expected = "
\n

Inline + ++, HTML tag glob + ++, JavaDoc tag + ++

\n
"; - - final String asciiDoc = parser.parseConfigDescription(javaDoc); - final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); - assertEquals(expected, actual); - } - - @ParameterizedTest - @ValueSource(strings = { "{", "}" }) - public void escapeBrackets(String ch) { - final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch - + ""; - final String expected = "
\n

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

\n
"; - - final String asciiDoc = parser.parseConfigDescription(javaDoc); - final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); - assertEquals(expected, actual); - } - - @Test - void trim() { - assertEquals("+ \nfoo", JavaDocParser.trim(new StringBuilder("+ \nfoo"))); - assertEquals("+", JavaDocParser.trim(new StringBuilder(" +"))); - assertEquals("foo", JavaDocParser.trim(new StringBuilder(" +\nfoo"))); - assertEquals("foo +", JavaDocParser.trim(new StringBuilder("foo +"))); - assertEquals("foo", JavaDocParser.trim(new StringBuilder("foo"))); - assertEquals("+", JavaDocParser.trim(new StringBuilder("+ \n"))); - assertEquals("+", JavaDocParser.trim(new StringBuilder(" +\n+ \n"))); - assertEquals("", JavaDocParser.trim(new StringBuilder(" +\n"))); - assertEquals("foo", JavaDocParser.trim(new StringBuilder(" \n\tfoo"))); - assertEquals("foo", JavaDocParser.trim(new StringBuilder("foo \n\t"))); - assertEquals("foo", JavaDocParser.trim(new StringBuilder(" \n\tfoo \n\t"))); - assertEquals("", JavaDocParser.trim(new StringBuilder(""))); - assertEquals("", JavaDocParser.trim(new StringBuilder(" \n\t"))); - assertEquals("+", JavaDocParser.trim(new StringBuilder(" +"))); - assertEquals("", JavaDocParser.trim(new StringBuilder(" +\n"))); - assertEquals("", JavaDocParser.trim(new StringBuilder(" +\n +\n"))); - assertEquals("foo +\nbar", JavaDocParser.trim(new StringBuilder(" foo +\nbar +\n"))); - } - -} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigSectionParserTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigSectionParserTest.java deleted file mode 100644 index bae3690f84962a..00000000000000 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigSectionParserTest.java +++ /dev/null @@ -1,150 +0,0 @@ -package io.quarkus.annotation.processor.generate_doc; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class JavaDocConfigSectionParserTest { - - private JavaDocParser parser; - - @BeforeEach - public void setup() { - parser = new JavaDocParser(); - } - - @Test - public void parseNullSection() { - JavaDocParser.SectionHolder parsed = parser.parseConfigSection(null, 1); - assertEquals("", parsed.details); - assertEquals("", parsed.title); - } - - @Test - public void parseUntrimmedJavaDoc() { - JavaDocParser.SectionHolder parsed = parser.parseConfigSection(" ", 1); - assertEquals("", parsed.details); - assertEquals("", parsed.title); - - parsed = parser.parseConfigSection("

", 1); - assertEquals("", parsed.details); - assertEquals("", parsed.title); - } - - @Test - public void passThroughAConfigSectionInAsciiDoc() { - String asciidoc = "=== My Asciidoc\n" + - "\n" + - ".Let's have a https://quarkus.io[link to our website].\n" + - "\n" + - "[TIP]\n" + - "====\n" + - "A nice tip\n" + - "====\n" + - "\n" + - "[source,java]\n" + - "----\n" + - "And some code\n" + - "----"; - - JavaDocParser.SectionHolder sectionHolder = parser.parseConfigSection(asciidoc + "\n" + "@asciidoclet", 1); - assertEquals(asciidoc, sectionHolder.details); - assertEquals("My Asciidoc", sectionHolder.title); - - asciidoc = "Asciidoc title. \n" + - "\n" + - "Let's have a https://quarkus.io[link to our website].\n" + - "\n" + - "[TIP]\n" + - "====\n" + - "A nice tip\n" + - "====\n" + - "\n" + - "[source,java]\n" + - "----\n" + - "And some code\n" + - "----"; - - sectionHolder = parser.parseConfigSection(asciidoc + "\n" + "@asciidoclet", 1); - assertEquals("Asciidoc title", sectionHolder.title); - } - - @Test - public void parseSectionWithoutIntroduction() { - /** - * Simple javadoc - */ - String javaDoc = "Config Section"; - String expectedTitle = "Config Section"; - String expectedDetails = "== Config Section"; - JavaDocParser.SectionHolder sectionHolder = parser.parseConfigSection(javaDoc, 1); - assertEquals(expectedDetails, sectionHolder.details); - assertEquals(expectedTitle, sectionHolder.title); - - javaDoc = "Config Section."; - expectedTitle = "Config Section"; - expectedDetails = "== Config Section"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 1).details); - assertEquals(expectedTitle, sectionHolder.title); - - /** - * html javadoc - */ - javaDoc = "

Config Section

"; - expectedTitle = "Config Section"; - expectedDetails = "== Config Section"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 1).details); - assertEquals(expectedTitle, sectionHolder.title); - } - - @Test - public void parseSectionWithIntroduction() { - /** - * Simple javadoc - */ - String javaDoc = "Config Section .Introduction"; - String expectedDetails = "== Config Section\n\nIntroduction"; - String expectedTitle = "Config Section"; - assertEquals(expectedTitle, parser.parseConfigSection(javaDoc, 1).title); - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 1).details); - - /** - * html javadoc - */ - javaDoc = "

Config Section

. Introduction"; - expectedDetails = "== Config Section\n\nIntroduction"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 1).details); - assertEquals(expectedTitle, parser.parseConfigSection(javaDoc, 1).title); - } - - @Test - public void properlyParseConfigSectionWrittenInHtml() { - String javaDoc = "

Config Section.

This is section introduction"; - String expectedDetails = "== Config Section\n\nThis is section introduction"; - String title = "Config Section"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 1).details); - assertEquals(title, parser.parseConfigSection(javaDoc, 1).title); - } - - @Test - public void handleSectionLevelCorrectly() { - String javaDoc = "

Config Section.

This is section introduction"; - - // level 0 should default to 1 - String expectedDetails = "= Config Section\n\nThis is section introduction"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 0).details); - - // level 1 - expectedDetails = "== Config Section\n\nThis is section introduction"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 1).details); - - // level 2 - expectedDetails = "=== Config Section\n\nThis is section introduction"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 2).details); - - // level 3 - expectedDetails = "==== Config Section\n\nThis is section introduction"; - assertEquals(expectedDetails, parser.parseConfigSection(javaDoc, 3).details); - } -} diff --git a/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider deleted file mode 100644 index 9582882517a776..00000000000000 --- a/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider +++ /dev/null @@ -1 +0,0 @@ -io.quarkus.annotation.processor.fs.CustomMemoryFileSystemProvider \ No newline at end of file diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationConfig.java index 3549077ef4c166..b7a52d70a5a09d 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationConfig.java @@ -6,6 +6,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Application + */ @ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) public class ApplicationConfig { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java index a410463dba5718..33d17d3d78fe39 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; @@ -24,11 +23,11 @@ import io.quarkus.bootstrap.logging.InitialConfigurator; import io.quarkus.bootstrap.runner.RunnerClassLoader; -import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.runtime.graal.DiagnosticPrinter; import io.quarkus.runtime.util.ExceptionUtil; import io.quarkus.runtime.util.StringUtil; +import io.smallrye.config.ConfigValidationException; import sun.misc.Signal; import sun.misc.SignalHandler; @@ -51,7 +50,7 @@ public class ApplicationLifecycleManager { // used by ShutdownEvent to propagate the information about shutdown reason public static volatile ShutdownEvent.ShutdownReason shutdownReason = ShutdownEvent.ShutdownReason.STANDARD; - private static volatile BiConsumer defaultExitCodeHandler = new BiConsumer() { + private static final BiConsumer MAIN_EXIT_CODE_HANDLER = new BiConsumer<>() { @Override public void accept(Integer integer, Throwable cause) { Logger logger = Logger.getLogger(Application.class); @@ -62,6 +61,12 @@ public void accept(Integer integer, Throwable cause) { System.exit(integer); } }; + private static final Consumer NOOP_ALREADY_STARTED_CALLBACK = new Consumer<>() { + @Override + public void accept(Boolean t) { + } + }; + private static volatile BiConsumer defaultExitCodeHandler = MAIN_EXIT_CODE_HANDLER; private ApplicationLifecycleManager() { @@ -77,8 +82,9 @@ private ApplicationLifecycleManager() { private static int exitCode = -1; private static volatile boolean shutdownRequested; - private static Application currentApplication; + private static volatile Application currentApplication; private static boolean vmShuttingDown; + private static Consumer alreadyStartedCallback = NOOP_ALREADY_STARTED_CALLBACK; private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("mac"); @@ -89,17 +95,19 @@ public static void run(Application application, String... args) { public static void run(Application application, Class quarkusApplication, BiConsumer exitCodeHandler, String... args) { + boolean alreadyStarted; stateLock.lock(); - //in tests, we might pass this method an already started application - //in this case we don't shut it down at the end - boolean alreadyStarted = application.isStarted(); - if (shutdownHookThread == null) { - registerHooks(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler); - } - if (currentApplication != null && !shutdownRequested) { - throw new IllegalStateException("Quarkus already running"); - } try { + //in tests, we might pass this method an already started application + //in this case we don't shut it down at the end + alreadyStarted = application.isStarted(); + alreadyStartedCallback.accept(alreadyStarted); + if (shutdownHookThread == null) { + registerHooks(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler); + } + if (currentApplication != null && !shutdownRequested) { + throw new IllegalStateException("Quarkus already running"); + } exitCode = -1; shutdownRequested = false; currentApplication = application; @@ -183,21 +191,18 @@ public static void run(Application application, Class'."); } - } else if (ExceptionUtil.isAnyCauseInstanceOf(e, ConfigurationException.class)) { + } else if (rootCause instanceof ConfigurationException || rootCause instanceof ConfigValidationException) { System.err.println(rootCause.getMessage()); - e.printStackTrace(); } else if (rootCause instanceof PreventFurtherStepsException && !StringUtil.isNullOrEmpty(rootCause.getMessage())) { System.err.println(rootCause.getMessage()); } else { - // If it is not a ConfigurationException it should be safe to call ConfigProvider.getConfig here - applicationLogger.errorv(e, "Failed to start application (with profile {0})", - ConfigUtils.getProfiles()); + applicationLogger.errorv(e, "Failed to start application"); ensureConsoleLogsDrained(); } } @@ -212,6 +217,7 @@ public static void run(Application application, Class defaultExitCodeHandler) { - Objects.requireNonNull(defaultExitCodeHandler); + if (defaultExitCodeHandler == null) { + defaultExitCodeHandler = MAIN_EXIT_CODE_HANDLER; + } ApplicationLifecycleManager.defaultExitCodeHandler = defaultExitCodeHandler; } @@ -367,8 +376,18 @@ public static void setDefaultExitCodeHandler(BiConsumer defa * * @param defaultExitCodeHandler the new default exit handler */ + // Used by StartupActionImpl via reflection public static void setDefaultExitCodeHandler(Consumer defaultExitCodeHandler) { - setDefaultExitCodeHandler((exitCode, cause) -> defaultExitCodeHandler.accept(exitCode)); + BiConsumer biConsumer = defaultExitCodeHandler == null ? null + : (exitCode, cause) -> defaultExitCodeHandler.accept(exitCode); + setDefaultExitCodeHandler(biConsumer); + } + + @SuppressWarnings("unused") + // Used by StartupActionImpl via reflection + public static void setAlreadyStartedCallback(Consumer alreadyStartedCallback) { + ApplicationLifecycleManager.alreadyStartedCallback = alreadyStartedCallback != null ? alreadyStartedCallback + : NOOP_ALREADY_STARTED_CALLBACK; } /** @@ -431,13 +450,18 @@ public void run() { } finally { stateLock.unlock(); } - if (currentApplication.isStarted()) { - // On CLI apps, SIGINT won't call io.quarkus.runtime.Application#stop(), - // making the awaitShutdown() below block the application termination process - // It should be a noop if called twice anyway - currentApplication.stop(); + //take a reliable reference before changing the application state: + final Application app = currentApplication; + if (app != null) { + if (app.isStarted()) { + // On CLI apps, SIGINT won't call io.quarkus.runtime.Application#stop(), + // making the awaitShutdown() below block the application termination process + // It should be a noop if called twice anyway + app.stop(); + } + app.awaitShutdown(); } - currentApplication.awaitShutdown(); + currentApplication = null; System.out.flush(); System.err.flush(); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/BannerRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/BannerRuntimeConfig.java index 3c141558de65d8..d19faeb0f827f6 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/BannerRuntimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/BannerRuntimeConfig.java @@ -4,6 +4,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Banner + */ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class BannerRuntimeConfig { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java index 73929157752bb6..0c15037a195c51 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java @@ -7,7 +7,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** - * Build time analytics configuration. + * Build time analytics. + *

* This is a dummy config class to hide the warnings on the comment line. * All properties in here are actually used in the build tools. */ diff --git a/core/runtime/src/main/java/io/quarkus/runtime/BuilderConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/BuilderConfig.java index b23b2cbe9ebdeb..4cc0e96c0dd5fc 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/BuilderConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/BuilderConfig.java @@ -7,6 +7,8 @@ import io.smallrye.config.ConfigMapping; /** + * Builder. + *

* This configuration class is here to avoid warnings when using {@code -Dquarkus.builder.=...}. * * @see io.quarkus.builder.BuildChainBuilder diff --git a/core/runtime/src/main/java/io/quarkus/runtime/CommandLineRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/CommandLineRuntimeConfig.java index ebb99d0ebe13c3..f6b5d4440aba02 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/CommandLineRuntimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/CommandLineRuntimeConfig.java @@ -2,14 +2,18 @@ import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocPrefix; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; /** + * Command line. + *

* This configuration class is here to avoid warnings when using {@code -Dquarkus.args=...}. */ @ConfigRoot(name = ConfigItem.PARENT, phase = ConfigPhase.RUN_TIME) +@ConfigDocPrefix("quarkus.command-line") public class CommandLineRuntimeConfig { /** diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java index 3289d992a7280b..54ca69c231cc60 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java @@ -4,12 +4,15 @@ import java.util.List; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocPrefix; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithName; /** + * Configuration. + *

* We don't really use this, because these are configurations for the config itself, so it causes a chicken / egg * problem, but we need it for documentation purposes. *
@@ -18,6 +21,7 @@ */ @ConfigMapping(prefix = "quarkus") @ConfigRoot(phase = ConfigPhase.RUN_TIME) +@ConfigDocPrefix("quarkus.config") public interface ConfigConfig { /** * A comma separated list of profiles that will be active when Quarkus launches. diff --git a/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java index 129742b14af230..cc756e52d97210 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java @@ -4,6 +4,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Debugging. + */ @ConfigRoot(name = "debug", phase = ConfigPhase.RUN_TIME) public class DebugRuntimeConfig { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ErrorPageAction.java b/core/runtime/src/main/java/io/quarkus/runtime/ErrorPageAction.java new file mode 100644 index 00000000000000..4dc3967abf23ab --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/ErrorPageAction.java @@ -0,0 +1,5 @@ +package io.quarkus.runtime; + +public record ErrorPageAction(String name, String url) { + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java index 8cb7ba3acbfcf9..47bc631081a2bb 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java @@ -17,6 +17,7 @@ import org.wildfly.common.cpu.ProcessorInfo; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.util.NoopShutdownScheduledExecutorService; /** * @@ -57,8 +58,19 @@ public void run() { if (threadPoolConfig.prefill) { underlying.prestartAllCoreThreads(); } - current = underlying; - return underlying; + ScheduledExecutorService managed = underlying; + // In prod and test mode, we wrap the ExecutorService and the shutdown() and shutdownNow() are deliberately not delegated + // This is to prevent the application and other extensions from shutting down the executor service + // The problem was described in https://github.com/quarkusio/quarkus/issues/16833#issuecomment-1917042589 + // and https://github.com/quarkusio/quarkus/issues/43228 + // For example, the Vertx instance is closed before io.quarkus.runtime.ExecutorRecorder.createShutdownTask() is used + // And when it's closed the underlying worker thread pool (which is in the prod mode backed by the ExecutorBuildItem) is closed as well + // As a result the quarkus.thread-pool.shutdown-interrupt config property and logic defined in ExecutorRecorder.createShutdownTask() is completely ignored + if (launchMode != LaunchMode.DEVELOPMENT) { + managed = new NoopShutdownScheduledExecutorService(underlying); + } + current = managed; + return managed; } private static Runnable createShutdownTask(ThreadPoolConfig threadPoolConfig, EnhancedQueueExecutor executor) { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java index 9e0a6ae50578e1..ec54fcb078fc82 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java @@ -5,6 +5,9 @@ import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; +/** + * Launch. + */ @ConfigMapping(prefix = "quarkus.launch") @ConfigRoot(phase = ConfigPhase.RUN_TIME) public interface LaunchConfig { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/LiveReloadConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/LiveReloadConfig.java index 28c18e42a972e5..e9c75a3da4a869 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/LiveReloadConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/LiveReloadConfig.java @@ -8,9 +8,18 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Live reload. + */ @ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) public class LiveReloadConfig { + /** + * Whether the live-reload feature should be enabled. + */ + @ConfigItem(defaultValue = "true") + boolean enabled; + /** * Whether Quarkus should enable its ability to not do a full restart * when changes to classes are compatible with JVM instrumentation. diff --git a/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java index deb6fd4dae7a35..7ca78d84ceeb0b 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java @@ -3,11 +3,16 @@ import java.util.Locale; import java.util.Set; +import io.quarkus.runtime.annotations.ConfigDocPrefix; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Localization. + */ @ConfigRoot(name = ConfigItem.PARENT, phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +@ConfigDocPrefix("quarkus.locales") public class LocalesBuildTimeConfig { // We set to en as the default language when all else fails since this is what the JDK does as well diff --git a/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java b/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java index 811f8d0a3ef714..a06f7f2063349a 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java @@ -15,10 +15,11 @@ public class StartupContext implements Closeable { private static final Logger LOG = Logger.getLogger(StartupContext.class); + // Holds values for returned proxies + // These values are usually returned from recorder methods but can be also set explicitly + // For example, the raw command line args and ShutdownContext are set when the StartupContext is created private final Map values = new HashMap<>(); - private Object lastValue; - // this is done to distinguish between the value having never been set and having been set as null - private boolean lastValueSet = false; + private final Deque shutdownTasks = new ConcurrentLinkedDeque<>(); private final Deque lastShutdownTasks = new ConcurrentLinkedDeque<>(); private String[] commandLineArgs; @@ -58,26 +59,17 @@ public String[] get() { public void putValue(String name, Object value) { values.put(name, value); - lastValueSet = true; - this.lastValue = value; } public Object getValue(String name) { return values.get(name); } - public Object getLastValue() { - return lastValue; - } - - public boolean isLastValueSet() { - return lastValueSet; - } - @Override public void close() { runAllAndClear(shutdownTasks); runAllAndClear(lastShutdownTasks); + values.clear(); } private void runAllAndClear(Deque tasks) { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java index 1d080ab8be9f8a..0a312ed871bd47 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java @@ -3,11 +3,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Scanner; import io.quarkus.dev.config.CurrentConfig; +import io.quarkus.runtime.logging.DecorateStackUtil; import io.quarkus.runtime.util.ExceptionUtil; public class TemplateHtmlBuilder { @@ -41,6 +43,14 @@ public class TemplateHtmlBuilder { " }\n" + "\n"; + private static final String HTML_TEMPLATE_START_NO_STACK = "" + + "\n" + + "\n" + + "\n" + + " %1$s%2$s\n" + + " \n" + + " "; + private static final String HTML_TEMPLATE_START = "" + "\n" + "\n" + @@ -115,10 +125,15 @@ public class TemplateHtmlBuilder { + "

\n" + "
\n" + "

%2$s

\n" + + "
%3$s
\n" + "
\n" + "
\n" + "
\n"; + private static final String HEADER_TEMPLATE_NO_STACK = "

%1$s

\n" + + "%2$s \n" + + "
\n"; + private static final String RESOURCES_START = "
%1$s
"; private static final String ANCHOR_TEMPLATE_ABSOLUTE = "%2$s"; @@ -145,6 +160,11 @@ public class TemplateHtmlBuilder { private static final String STACKTRACE_DISPLAY_DIV = "
"; + private static final String BRSTI = "___begin_relative_stack_trace_item___"; + private static final String ERSTI = "___end_relative_stack_trace_item___"; + + private static final String OPEN_IDE_LINK = "
"; + private static final String ERROR_STACK = "
\n" + "

The stacktrace below is the original. " + "See the stacktrace in reversed order (root-cause first)

" @@ -158,6 +178,7 @@ public class TemplateHtmlBuilder { "
%1$s
\n" + "
\n"; + private static final String DECORATE_DIV = "
%s
"; private static final String CONFIG_EDITOR_HEAD = "

The following incorrect config values were detected:

" + "
" + "\n"; @@ -176,25 +197,53 @@ public class TemplateHtmlBuilder { private String baseUrl; public TemplateHtmlBuilder(String title, String subTitle, String details) { - this(null, title, subTitle, details, null, Collections.emptyList()); + this(true, null, title, subTitle, details, Collections.emptyList(), null, Collections.emptyList()); } - public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String details) { - this(baseUrl, title, subTitle, details, null, Collections.emptyList()); + public TemplateHtmlBuilder(boolean showStack, String title, String subTitle, String details, + List actions) { + this(showStack, null, title, subTitle, details, actions, null, Collections.emptyList()); } - public TemplateHtmlBuilder(String title, String subTitle, String details, String redirect, + public TemplateHtmlBuilder(String title, String subTitle, String details, + List actions) { + this(true, null, title, subTitle, details, actions, null, Collections.emptyList()); + } + + public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String details, + List actions) { + this(true, baseUrl, title, subTitle, details, actions, null, Collections.emptyList()); + } + + public TemplateHtmlBuilder(String title, String subTitle, String details, List actions, + String redirect, List config) { - this(null, title, subTitle, details, null, Collections.emptyList()); + this(true, null, title, subTitle, details, actions, null, Collections.emptyList()); } - public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String details, String redirect, + public TemplateHtmlBuilder(boolean showStack, String baseUrl, String title, String subTitle, String details, + List actions, + String redirect, List config) { this.baseUrl = baseUrl; - loadCssFile(); - result = new StringBuilder(String.format(HTML_TEMPLATE_START, escapeHtml(title), - subTitle == null || subTitle.isEmpty() ? "" : " - " + escapeHtml(subTitle), CSS)); - result.append(String.format(HEADER_TEMPLATE, escapeHtml(title), escapeHtml(details))); + StringBuilder actionLinks = new StringBuilder(); + + if (showStack) { + loadCssFile(); + for (ErrorPageAction epa : actions) { + actionLinks.append(buildLink(epa.name(), epa.url())); + } + + result = new StringBuilder(String.format(HTML_TEMPLATE_START, escapeHtml(title), + subTitle == null || subTitle.isEmpty() ? "" : " - " + escapeHtml(subTitle), CSS)); + result.append(String.format(HEADER_TEMPLATE, escapeHtml(title), escapeHtml(details), actionLinks.toString())); + } else { + result = new StringBuilder(String.format(HTML_TEMPLATE_START_NO_STACK, escapeHtml(title), + subTitle == null || subTitle.isEmpty() ? "" : " - " + escapeHtml(subTitle), CSS)); + result.append( + String.format(HEADER_TEMPLATE_NO_STACK, escapeHtml(title), escapeHtml(details), actionLinks.toString())); + } + if (!config.isEmpty()) { result.append(String.format(CONFIG_EDITOR_HEAD, redirect)); for (CurrentConfig i : config) { @@ -205,10 +254,66 @@ public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String } } + public TemplateHtmlBuilder decorate(final Throwable throwable, String srcMainJava, List knowClasses) { + String decoratedString = DecorateStackUtil.getDecoratedString(throwable, srcMainJava, knowClasses); + if (decoratedString != null) { + result.append(String.format(DECORATE_DIV, decoratedString)); + } + + return this; + } + public TemplateHtmlBuilder stack(final Throwable throwable) { - result.append(String.format(ERROR_STACK, escapeHtml(ExceptionUtil.generateStackTrace(throwable)))); - result.append(String.format(ERROR_STACK_REVERSED, escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable)))); - result.append(STACKTRACE_DISPLAY_DIV); + return stack(throwable, List.of()); + } + + public TemplateHtmlBuilder stack(final Throwable throwable, List knowClasses) { + if (knowClasses != null && throwable != null) { + StackTraceElement[] originalStackTrace = Arrays.copyOf(throwable.getStackTrace(), throwable.getStackTrace().length); + StackTraceElement[] stackTrace = throwable.getStackTrace(); + String className = ""; + String type = "java"; //default + int lineNumber = 0; + if (!knowClasses.isEmpty()) { + + for (int i = 0; i < stackTrace.length; ++i) { + var elem = stackTrace[i]; + if (knowClasses.contains(elem.getClassName())) { + className = elem.getClassName(); + String filename = elem.getFileName(); + if (filename != null) { + int dotindex = filename.lastIndexOf("."); + type = elem.getFileName().substring(dotindex + 1); + } + lineNumber = elem.getLineNumber(); + + stackTrace[i] = new StackTraceElement(elem.getClassLoaderName(), elem.getModuleName(), + elem.getModuleVersion(), + BRSTI + elem.getClassName() + + ERSTI, + elem.getMethodName(), elem.getFileName(), elem.getLineNumber()); + } + } + } + throwable.setStackTrace(stackTrace); + + String original = escapeHtml(ExceptionUtil.generateStackTrace(throwable)); + String rootFirst = escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable)); + if (original.contains(BRSTI)) { + original = original.replace(BRSTI, + String.format(OPEN_IDE_LINK, className, type, lineNumber)); + original = original.replace(ERSTI, "
"); + rootFirst = rootFirst.replace(BRSTI, + String.format(OPEN_IDE_LINK, className, type, lineNumber)); + rootFirst = rootFirst.replace(ERSTI, "
"); + } + + result.append(String.format(ERROR_STACK, original)); + result.append(String.format(ERROR_STACK_REVERSED, rootFirst)); + result.append(STACKTRACE_DISPLAY_DIV); + + throwable.setStackTrace(originalStackTrace); + } return this; } @@ -240,7 +345,7 @@ public TemplateHtmlBuilder staticResourcePath(String title, String description) } public TemplateHtmlBuilder servletMapping(String title) { - return resourcePath(title, false, false, null); + return resourcePath(title, false, true, null); } private TemplateHtmlBuilder resourcePath(String title, boolean withListStart, boolean withAnchor, String description) { @@ -379,4 +484,8 @@ public void loadCssFile() { } } } + + private String buildLink(String name, String url) { + return "" + name + ""; + } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ThreadPoolConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/ThreadPoolConfig.java index 818863c72dd112..df07263a149433 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ThreadPoolConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ThreadPoolConfig.java @@ -9,6 +9,8 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** + * Core thread pool. + *

* The core thread pool config. This thread pool is responsible for running * all blocking tasks. */ diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TlsConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/TlsConfig.java deleted file mode 100644 index eb75cfa60af9de..00000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/TlsConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.quarkus.runtime; - -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; - -/** - * Configuration class allowing to globally set TLS properties. - */ -@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) -public class TlsConfig { - - /** - * Enable trusting all certificates. Disable by default. - */ - @ConfigItem(defaultValue = "false") - public boolean trustAll; - - @Override - public String toString() { - return "TlsConfig{" + - "trustAll=" + trustAll + - '}'; - } -} \ No newline at end of file diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocDefault.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocDefault.java index b96723d2bbc472..ece03bd17604e4 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocDefault.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocDefault.java @@ -3,7 +3,7 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -17,7 +17,7 @@ * Replaces defaultValueForDocumentation for the {@link ConfigMapping} approach. */ @Documented -@Retention(SOURCE) +@Retention(RUNTIME) @Target({ FIELD, PARAMETER, METHOD }) public @interface ConfigDocDefault { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocEnum.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocEnum.java new file mode 100644 index 00000000000000..99d99559e95d1f --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocEnum.java @@ -0,0 +1,24 @@ +package io.quarkus.runtime.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Provides a way to configure how an enum is handled. + */ +@Retention(RUNTIME) +@Target({ FIELD, METHOD }) +@Documented +public @interface ConfigDocEnum { + + /** + * This can be used to enforce hyphenating the enum values even if a converter is present. + */ + boolean enforceHyphenateValues() default false; + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocFilename.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocFilename.java index d8489faaeaede1..db67cda5497a31 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocFilename.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocFilename.java @@ -1,6 +1,6 @@ package io.quarkus.runtime.annotations; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -14,7 +14,7 @@ * If not specified, the effective file name is derived either from the class name or {@link ConfigMapping#prefix()}. */ @Documented -@Retention(SOURCE) +@Retention(RUNTIME) @Target({ ElementType.TYPE }) public @interface ConfigDocFilename { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocIgnore.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocIgnore.java index 1bc1a8469c9e20..ee67e4c5bfa8cb 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocIgnore.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocIgnore.java @@ -1,7 +1,7 @@ package io.quarkus.runtime.annotations; import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -12,7 +12,7 @@ * when generating documentation. */ @Documented -@Retention(SOURCE) +@Retention(RUNTIME) @Target({ METHOD }) public @interface ConfigDocIgnore { } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocMapKey.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocMapKey.java index 74e6b54fd62a60..f90353e882b2fe 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocMapKey.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocMapKey.java @@ -3,7 +3,7 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -13,7 +13,7 @@ * A marker indicating a user-friendly documentation key for the {@link java.util.Map} type. */ @Documented -@Retention(SOURCE) +@Retention(RUNTIME) @Target({ FIELD, PARAMETER, METHOD }) public @interface ConfigDocMapKey { String value(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocPrefix.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocPrefix.java new file mode 100644 index 00000000000000..befc4bc97c211e --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocPrefix.java @@ -0,0 +1,24 @@ +package io.quarkus.runtime.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * This annotation can be used when you want to override the top level prefix from the ConfigRoot/ConfigMapping for doc + * generation. + *

+ * This is for instance useful for {@code ConfigConfig}, which is an odd beast. + *

+ * Should be considered very last resort. + */ +@Documented +@Retention(RUNTIME) +@Target({ ElementType.TYPE }) +public @interface ConfigDocPrefix { + + String value(); +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocSection.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocSection.java index 372cc7782d6218..08544ac6e861bf 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocSection.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocSection.java @@ -3,7 +3,7 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -14,7 +14,16 @@ * The section will be generated only if the configuration item type is annotated with {@link ConfigGroup} */ @Documented -@Retention(SOURCE) +@Retention(RUNTIME) @Target({ FIELD, PARAMETER, METHOD }) public @interface ConfigDocSection { + + /** + * If we should generate a specific file for this section. + *

+ * We used to do it for all config groups before but it's counterproductive. + * The new annotation processor only generates a file for a config group + * if this is true. + */ + boolean generated() default false; } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/RegisterForProxy.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RegisterForProxy.java new file mode 100644 index 00000000000000..fda3b86ecbd314 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RegisterForProxy.java @@ -0,0 +1,39 @@ +package io.quarkus.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that can be used to force an interface (including its super interfaces) to be registered for dynamic proxy + * generation in native image mode. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(RegisterForProxy.List.class) +public @interface RegisterForProxy { + + /** + * Alternative interfaces that should actually be registered for dynamic proxy generation instead of the current interface. + * This allows for interfaces in 3rd party libraries to be registered without modification or writing an + * extension. If this is set then the interface it is placed on is not registered for dynamic proxy generation, so this + * should generally just be placed on an empty interface that is not otherwise used. + */ + Class[] targets() default {}; + + /** + * The repeatable holder for {@link RegisterForProxy}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface List { + /** + * The {@link RegisterForProxy} instances. + * + * @return the instances + */ + RegisterForProxy[] value(); + } +} \ No newline at end of file diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ApplicationPropertiesConfigSourceLoader.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ApplicationPropertiesConfigSourceLoader.java deleted file mode 100644 index 118662e7ac750c..00000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ApplicationPropertiesConfigSourceLoader.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.quarkus.runtime.configuration; - -import java.io.IOException; -import java.net.URI; -import java.net.URL; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; - -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.ConfigSourceProvider; - -import io.smallrye.config.AbstractLocationConfigSourceLoader; -import io.smallrye.config.PropertiesConfigSource; - -public class ApplicationPropertiesConfigSourceLoader extends AbstractLocationConfigSourceLoader { - @Override - protected String[] getFileExtensions() { - return new String[] { "properties" }; - } - - @Override - protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException { - return new PropertiesConfigSource(url, ordinal); - } - - public static class InClassPath extends ApplicationPropertiesConfigSourceLoader implements ConfigSourceProvider { - @Override - public List getConfigSources(final ClassLoader classLoader) { - return loadConfigSources("application.properties", 250, classLoader); - } - - @Override - protected List tryFileSystem(final URI uri, final int ordinal) { - return Collections.emptyList(); - } - } - - public static class InFileSystem extends ApplicationPropertiesConfigSourceLoader implements ConfigSourceProvider { - @Override - public List getConfigSources(final ClassLoader classLoader) { - return loadConfigSources( - Paths.get(System.getProperty("user.dir"), "config", "application.properties").toUri().toString(), 260, - classLoader); - } - - @Override - protected List tryClassPath(final URI uri, final int ordinal, final ClassLoader classLoader) { - return Collections.emptyList(); - } - } -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java index 6b80e16ba3dde7..016be508619052 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java @@ -13,10 +13,10 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.ConfigValue; import org.eclipse.microprofile.config.spi.ConfigSource; import io.quarkus.runtime.LaunchMode; -import io.smallrye.config.DotEnvConfigSourceProvider; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -80,10 +80,7 @@ public static SmallRyeConfigBuilder emptyConfigBuilder() { .addDefaultInterceptors() .addDiscoveredInterceptors() .addDiscoveredSecretKeysHandlers() - .addDefaultSources() - .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) - .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) - .withSources(new DotEnvConfigSourceProvider()); + .addDefaultSources(); } public static List getProfiles() { @@ -111,6 +108,19 @@ public static boolean isPropertyPresent(String propertyName) { return ConfigProvider.getConfig().unwrap(SmallRyeConfig.class).isPropertyPresent(propertyName); } + /** + * Checks if a property has non-empty value in the current Configuration. + *

+ * This method is similar to {@link #isPropertyPresent(String)}, but does not ignore expression expansion. + * + * @param propertyName the property name. + * @return true if the property is present or false otherwise. + */ + public static boolean isPropertyNonEmpty(String propertyName) { + ConfigValue configValue = ConfigProvider.getConfig().getConfigValue(propertyName); + return configValue.getValue() != null && !configValue.getValue().isEmpty(); + } + /** * Checks if any of the given properties is present in the current Configuration. *

diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ProfileManager.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ProfileManager.java deleted file mode 100644 index 68dc7a87696ce0..00000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ProfileManager.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.quarkus.runtime.configuration; - -import io.quarkus.runtime.LaunchMode; - -/** - * Class that is responsible for resolving the current profile - * - * As this is needed immediately after startup it does not use any of the usual build/config infrastructure. - * - * The profile is resolved in the following way: - * - *

    - *
  • The quarkus.profile system property
  • - *
  • The QUARKUS_PROFILE environment entry
  • - *
  • The default runtime profile provided during build
  • - *
  • The default property for the launch mode
  • - *
- * - */ -public class ProfileManager { - private static volatile LaunchMode launchMode = LaunchMode.NORMAL; - - public static void setLaunchMode(LaunchMode mode) { - launchMode = mode; - } - - public static LaunchMode getLaunchMode() { - return launchMode; - } - - //NOTE: changes made here must be replicated in BootstrapProfile - -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java index c2d6b78f1e2754..0a7b27e0f52cfc 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java @@ -76,34 +76,47 @@ public static Iterable filterPropertiesInRoots(final Iterable pr continue; } - for (String root : roots) { - // if property is less than the root no way to match - if (property.length() < root.length()) { - continue; - } - - // if it is the same, then it can still map with parent name - if (property.equals(root)) { - matchedProperties.add(property); - break; - } else if (property.length() == root.length()) { - continue; - } - - // foo.bar - // foo.bar."baz" - // foo.bar[0] - char c = property.charAt(root.length()); - if ((c == '.') || c == '[') { - if (property.startsWith(root)) { - matchedProperties.add(property); - } - } + if (isPropertyInRoots(property, roots)) { + matchedProperties.add(property); } } return matchedProperties; } + public static boolean isPropertyInRoots(final String property, final Set roots) { + for (String root : roots) { + if (isPropertyInRoot(property, root)) { + return true; + } + } + return false; + } + + public static boolean isPropertyInRoot(final String property, final String root) { + // if property is less than the root no way to match + if (property.length() < root.length()) { + return false; + } + + // if it is the same, then it can still map with parent name + if (property.equals(root)) { + return true; + } + + if (property.length() == root.length()) { + return false; + } + + // foo.bar + // foo.bar."baz" + // foo.bar[0] + char c = property.charAt(root.length()); + if ((c == '.') || c == '[') { + return property.startsWith(root); + } + return false; + } + public static boolean isPropertyQuarkusCompoundName(NameIterator propertyName) { return propertyName.getName().startsWith("\"quarkus."); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java index d20c66f8a36f9b..5415f6e4d521ca 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/RuntimeConfigBuilder.java @@ -2,7 +2,6 @@ import java.util.UUID; -import io.smallrye.config.DotEnvConfigSourceProvider; import io.smallrye.config.SmallRyeConfigBuilder; import io.smallrye.config.SmallRyeConfigBuilderCustomizer; @@ -17,10 +16,7 @@ public void configBuilder(final SmallRyeConfigBuilder builder) { builder.forClassLoader(Thread.currentThread().getContextClassLoader()) .addDefaultInterceptors() - .addDefaultSources() - .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) - .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) - .withSources(new DotEnvConfigSourceProvider()); + .addDefaultSources(); } @Override diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java index d27c3bbb1e2e2e..a2bdbb63aef1de 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/StaticInitConfigBuilder.java @@ -1,6 +1,5 @@ package io.quarkus.runtime.configuration; -import io.smallrye.config.DotEnvConfigSourceProvider; import io.smallrye.config.SmallRyeConfigBuilder; import io.smallrye.config.SmallRyeConfigBuilderCustomizer; @@ -14,10 +13,7 @@ public void configBuilder(final SmallRyeConfigBuilder builder) { builder.forClassLoader(Thread.currentThread().getContextClassLoader()) .addDefaultInterceptors() - .addDefaultSources() - .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) - .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) - .withSources(new DotEnvConfigSourceProvider()); + .addDefaultSources(); } @Override diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/SystemOnlySourcesConfigBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/SystemOnlySourcesConfigBuilder.java index 34aa70b56f2be1..47e088f99bf174 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/SystemOnlySourcesConfigBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/SystemOnlySourcesConfigBuilder.java @@ -5,8 +5,7 @@ public class SystemOnlySourcesConfigBuilder implements ConfigBuilder { @Override public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) { - builder.getSourceProviders().clear(); - return builder; + return builder.setAddDefaultSources(false).addSystemSources(); } @Override diff --git a/core/runtime/src/main/java/io/quarkus/runtime/console/ConsoleRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/console/ConsoleRuntimeConfig.java index f1661f80603ba3..9821fa4cdf4c3b 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/console/ConsoleRuntimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/console/ConsoleRuntimeConfig.java @@ -6,6 +6,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Console + */ @ConfigRoot(name = "console", phase = ConfigPhase.RUN_TIME) public class ConsoleRuntimeConfig { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/init/InitRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/init/InitRuntimeConfig.java index 7e174cc0cfd3c2..d4476355d108fc 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/init/InitRuntimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/init/InitRuntimeConfig.java @@ -4,6 +4,7 @@ import org.eclipse.microprofile.config.spi.Converter; +import io.quarkus.runtime.annotations.ConfigDocPrefix; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; @@ -11,8 +12,12 @@ import io.smallrye.config.WithConverter; import io.smallrye.config.WithDefault; +/** + * Initialization + */ @ConfigMapping(prefix = "quarkus") @ConfigRoot(phase = ConfigPhase.RUN_TIME) +@ConfigDocPrefix("quarkus.init") public interface InitRuntimeConfig { /** diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java new file mode 100644 index 00000000000000..cdb68828c43c00 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java @@ -0,0 +1,93 @@ +package io.quarkus.runtime.logging; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class DecorateStackUtil { + + public static String getDecoratedString(final Throwable throwable, String srcMainJava, List knowClasses) { + if (srcMainJava != null) { + return DecorateStackUtil.getDecoratedString(throwable, Path.of(srcMainJava), knowClasses); + } + return null; + } + + public static String getDecoratedString(final Throwable throwable, Path srcMainJava, List knowClasses) { + if (knowClasses != null && !knowClasses.isEmpty() && throwable != null) { + StackTraceElement[] stackTrace = throwable.getStackTrace(); + for (int i = 0; i < stackTrace.length; ++i) { + StackTraceElement elem = stackTrace[i]; + if (knowClasses.contains(elem.getClassName())) { + String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem); + if (decoratedString != null) { + return decoratedString; + } + } + } + } + + return null; + } + + public static String getDecoratedString(Path srcMainJava, StackTraceElement stackTraceElement) { + int lineNumber = stackTraceElement.getLineNumber(); + if (lineNumber > 0 && srcMainJava != null) { + String fullJavaFileName = getFullPath(stackTraceElement.getClassName(), stackTraceElement.getFileName()); + Path f = srcMainJava.resolve(fullJavaFileName); + try { + List contextLines = DecorateStackUtil.getRelatedLinesInSource(f, lineNumber, 2); + if (contextLines != null) { + String header = "Exception in " + stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber(); + return header + "\n" + String.join("\n", contextLines); + } + } catch (IOException e) { + // Could not find the source for some reason. Just return nothing then + } + } + return null; + } + + private static List getRelatedLinesInSource(Path filePath, int lineNumber, int contextRange) throws IOException { + if (Files.exists(filePath)) { + List resultLines = new ArrayList<>(); + Deque contextQueue = new ArrayDeque<>(2 * contextRange + 1); + try (BufferedReader reader = Files.newBufferedReader(filePath)) { + String line; + int currentLine = 1; + while ((line = reader.readLine()) != null) { + if (currentLine >= lineNumber - contextRange) { + String ln = String.valueOf(currentLine); + if (currentLine == lineNumber) { + ln = "→ " + ln + " "; + } else { + ln = " " + ln + " "; + } + + contextQueue.add("\t" + ln + line); + } + if (currentLine >= lineNumber + contextRange) { + break; + } + currentLine++; + } + resultLines.addAll(contextQueue); + } + return resultLines; + } + return null; + } + + private static String getFullPath(String fullClassName, String fileName) { + int lastDotIndex = fullClassName.lastIndexOf("."); + String packageName = fullClassName.substring(0, lastDotIndex); + String path = packageName.replace('.', '/'); + return path + "/" + fileName; + } + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java index 69152905f18cf8..1ba1be87d240ac 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java @@ -2,6 +2,7 @@ import java.io.File; import java.nio.charset.Charset; +import java.time.format.DateTimeFormatter; import java.util.Optional; import java.util.logging.Level; @@ -84,6 +85,8 @@ public static class RotationConfig { * The file handler rotation file suffix. * When used, the file will be rotated based on its suffix. *

+ * The suffix must be in a date-time format that is understood by {@link DateTimeFormatter}. + *

* Example fileSuffix: .yyyy-MM-dd *

* Note: If the suffix ends with .zip or .gz, the rotation file will also be compressed. diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java index 0f9914985fccf9..a201118e2d6668 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java @@ -8,6 +8,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Logging + */ @ConfigRoot(name = "log", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) public class LogBuildTimeConfig { @@ -23,6 +26,12 @@ public class LogBuildTimeConfig { @ConfigItem(defaultValue = "DEBUG") public Level minLevel; + /** + * This will decorate the stacktrace in dev mode to show the line in the code that cause the exception + */ + @ConfigItem(defaultValue = "true") + public Boolean decorateStacktraces; + /** * Minimum logging categories. *

diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java index 6efcb787d19604..094b5a730e5428 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java @@ -11,7 +11,7 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** - * + * Logging */ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public final class LogConfig { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 6108893f04013b..1dd3b0e396c80d 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -269,7 +269,7 @@ public void accept(String name, String className) { try { nameToFilter.put(name, logFilterFactory.create(className)); } catch (Exception e) { - throw new RuntimeException("Unable to create instance of Logging Filter '" + className + "'"); + throw new RuntimeException("Unable to create instance of Logging Filter '" + className + "'", e); } } }); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownConfig.java index be79d04d1cf03a..431e1436d72441 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownConfig.java @@ -7,6 +7,9 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +/** + * Shutdown + */ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class ShutdownConfig { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownRecorder.java index ace0f19deb298d..6c969598473b7b 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownRecorder.java @@ -28,6 +28,9 @@ public void setListeners(List listeners, boolean delayEnabled) } public static void runShutdown() { + if (shutdownListeners == null) { // when QUARKUS_INIT_AND_EXIT is used, ShutdownRecorder#setListeners has not been called + return; + } log.debug("Attempting to gracefully shutdown."); try { executePreShutdown(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/types/GenericArrayTypeImpl.java b/core/runtime/src/main/java/io/quarkus/runtime/types/GenericArrayTypeImpl.java new file mode 100644 index 00000000000000..be64b08c69622c --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/types/GenericArrayTypeImpl.java @@ -0,0 +1,53 @@ +package io.quarkus.runtime.types; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; + +/** + * @author Marko Luksa + * @author Jozef Hartinger + */ +public class GenericArrayTypeImpl implements GenericArrayType { + + private Type genericComponentType; + + public GenericArrayTypeImpl(Type genericComponentType) { + this.genericComponentType = genericComponentType; + } + + public GenericArrayTypeImpl(Class rawType, Type... actualTypeArguments) { + this.genericComponentType = new ParameterizedTypeImpl(rawType, actualTypeArguments); + } + + @Override + public Type getGenericComponentType() { + return genericComponentType; + } + + @Override + public int hashCode() { + return ((genericComponentType == null) ? 0 : genericComponentType.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof GenericArrayType) { + GenericArrayType that = (GenericArrayType) obj; + if (genericComponentType == null) { + return that.getGenericComponentType() == null; + } else { + return genericComponentType.equals(that.getGenericComponentType()); + } + } else { + return false; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(genericComponentType.toString()); + sb.append("[]"); + return sb.toString(); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/types/ParameterizedTypeImpl.java b/core/runtime/src/main/java/io/quarkus/runtime/types/ParameterizedTypeImpl.java new file mode 100644 index 00000000000000..587d961b9c9b11 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/types/ParameterizedTypeImpl.java @@ -0,0 +1,86 @@ +package io.quarkus.runtime.types; + +import java.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; + +public class ParameterizedTypeImpl implements ParameterizedType, Serializable { + + private static final long serialVersionUID = -3005183010706452884L; + + private final Type[] actualTypeArguments; + private final Type rawType; + private final Type ownerType; + + public ParameterizedTypeImpl(Type rawType, Type... actualTypeArguments) { + this(rawType, actualTypeArguments, null); + } + + public ParameterizedTypeImpl(Type rawType, Type[] actualTypeArguments, Type ownerType) { + this.actualTypeArguments = actualTypeArguments; + this.rawType = rawType; + this.ownerType = ownerType; + } + + @Override + public Type[] getActualTypeArguments() { + return Arrays.copyOf(actualTypeArguments, actualTypeArguments.length); + } + + @Override + public Type getOwnerType() { + return ownerType; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public int hashCode() { + return Arrays.hashCode(actualTypeArguments) ^ (ownerType == null ? 0 : ownerType.hashCode()) + ^ (rawType == null ? 0 : rawType.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof ParameterizedType that) { + Type thatOwnerType = that.getOwnerType(); + Type thatRawType = that.getRawType(); + return (ownerType == null ? thatOwnerType == null : ownerType.equals(thatOwnerType)) + && (rawType == null ? thatRawType == null : rawType.equals(thatRawType)) + && Arrays.equals(actualTypeArguments, that.getActualTypeArguments()); + } else { + return false; + } + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (rawType instanceof Class) { + sb.append(((Class) rawType).getName()); + } else { + sb.append(rawType); + } + if (actualTypeArguments.length > 0) { + sb.append("<"); + for (Type actualType : actualTypeArguments) { + if (actualType instanceof Class) { + sb.append(((Class) actualType).getName()); + } else { + sb.append(actualType); + } + sb.append(", "); + } + sb.delete(sb.length() - 2, sb.length()); + sb.append(">"); + } + return sb.toString(); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/types/WildcardTypeImpl.java b/core/runtime/src/main/java/io/quarkus/runtime/types/WildcardTypeImpl.java new file mode 100644 index 00000000000000..1a4ac54beec991 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/types/WildcardTypeImpl.java @@ -0,0 +1,73 @@ +package io.quarkus.runtime.types; + +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; + +/** + * This code was mainly copied from Weld codebase. + * + * Implementation of {@link WildcardType}. + * + * Note that per JLS a wildcard may define either the upper bound or the lower bound. A wildcard may not have multiple bounds. + * + * @author Jozef Hartinger + * + */ +public class WildcardTypeImpl implements WildcardType { + + public static WildcardType defaultInstance() { + return DEFAULT_INSTANCE; + } + + public static WildcardType withUpperBound(Type type) { + return new WildcardTypeImpl(new Type[] { type }, DEFAULT_LOWER_BOUND); + } + + public static WildcardType withLowerBound(Type type) { + return new WildcardTypeImpl(DEFAULT_UPPER_BOUND, new Type[] { type }); + } + + private static final Type[] DEFAULT_UPPER_BOUND = new Type[] { Object.class }; + private static final Type[] DEFAULT_LOWER_BOUND = new Type[0]; + private static final WildcardType DEFAULT_INSTANCE = new WildcardTypeImpl(DEFAULT_UPPER_BOUND, DEFAULT_LOWER_BOUND); + + private final Type[] upperBound; + private final Type[] lowerBound; + + private WildcardTypeImpl(Type[] upperBound, Type[] lowerBound) { + this.upperBound = upperBound; + this.lowerBound = lowerBound; + } + + @Override + public Type[] getUpperBounds() { + return upperBound; + } + + @Override + public Type[] getLowerBounds() { + return lowerBound; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof WildcardType)) { + return false; + } + WildcardType other = (WildcardType) obj; + return Arrays.equals(lowerBound, other.getLowerBounds()) && Arrays.equals(upperBound, other.getUpperBounds()); + } + + @Override + public int hashCode() { + // We deliberately use the logic from JDK/guava + return Arrays.hashCode(lowerBound) ^ Arrays.hashCode(upperBound); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ExceptionUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ExceptionUtil.java index b27f844b3cf06a..99fbf4fdce04ce 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ExceptionUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ExceptionUtil.java @@ -86,17 +86,6 @@ public static Throwable getRootCause(Throwable exception) { return chain.isEmpty() ? null : chain.get(chain.size() - 1); } - public static boolean isAnyCauseInstanceOf(Throwable exception, Class classToCheck) { - Throwable curr = exception; - do { - if (classToCheck.isInstance(curr)) { - return true; - } - curr = curr.getCause(); - } while (curr != null); - return false; - } - /** * Creates and returns a new {@link Throwable} which has the following characteristics: *