diff --git a/.github/actions/common/action.yml b/.github/actions/common/action.yml new file mode 100644 index 00000000000..06916523fe1 --- /dev/null +++ b/.github/actions/common/action.yml @@ -0,0 +1,159 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: 'Common Job Steps' +description: A composite action that abstracts the common steps needed to implement a job +inputs: + native-image: + description: Whether to setup GraalVM native-image + required: false + default: 'false' + maven-cache: + description: Whether to cache the Maven local repository (read-only or read-write) + required: false + default: 'read-only' + build-cache: + description: Whether to cache the Maven build (read-only or read-write) + required: false + default: '' + build-cache-id: + description: Build cache id + required: false + default: 'default' + run: + description: The bash command to run + required: true + artifact-name: + description: Name of the artifact to create + required: false + default: '' + artifact-path: + description: Path of the files to include in the artifact + required: false + default: '' + test-artifact-name: + description: Name of the test artifact to create (excluded on windows), if non empty tests are archived + required: false + default: '' + free-space: + description: Whether to aggressively free disk space on the runner + default: 'false' +runs: + using: "composite" + steps: + - if: ${{ inputs.free-space == 'true' }} + # See https://github.com/actions/runner-images/issues/2840 + name: Free disk space + shell: bash + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/share/powershell + - if: ${{ runner.os == 'Windows' }} + name: Use GNU tar + shell: cmd + run: | + echo "Adding GNU tar to PATH" + echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + git config --global core.autocrlf false + git config --global core.eol lf + - name: Set up GraalVM + if: ${{ inputs.native-image == 'true' }} + uses: graalvm/setup-graalvm@v1.2.4 + with: + java-version: ${{ env.GRAALVM_VERSION || env.JAVA_VERSION }} + components: ${{ env.GRAALVM_COMPONENTS }} + check-for-updates: 'false' + set-java-home: 'false' + - name: Set up JDK + uses: actions/setup-java@v4.1.0 + with: + distribution: ${{ env.JAVA_DISTRO }} + java-version: ${{ env.JAVA_VERSION }} + - name: Cache local Maven repository (read-write) + if: ${{ inputs.maven-cache == 'read-write' }} + uses: actions/cache@v4.0.2 + with: + # See https://github.com/actions/toolkit/issues/713 + # Include must not match top level directories + path: | + .m2/repository/**/*.* + !.m2/repository/io/helidon/** + enableCrossOsArchive: true + # only hash top-level poms to keep it fast + key: local-maven-${{ hashFiles('*/pom.xml', 'pom.xml') }} + restore-keys: | + local-maven- + - name: Cache local Maven repository (read-only) + if: ${{ inputs.maven-cache == 'read-only' }} + uses: actions/cache/restore@v4.0.2 + with: + path: | + .m2/repository/**/*.* + !.m2/repository/io/helidon/** + enableCrossOsArchive: true + key: local-maven-${{ hashFiles('*/pom.xml', 'pom.xml') }} + restore-keys: | + local-maven- + - name: Build cache (read-write) + if: ${{ inputs.build-cache == 'read-write' }} + uses: actions/cache@v4.0.2 + with: + path: | + ./**/target/** + .m2/repository/io/helidon/** + enableCrossOsArchive: true + key: build-cache-${{ github.run_id }}-${{ github.run_attempt }}-${{ inputs.build-cache-id }} + restore-keys: | + build-cache-${{ github.run_id }}-${{ github.run_attempt }}- + build-cache-${{ github.run_id }}- + - name: Build cache (read-only) + if: ${{ inputs.build-cache == 'read-only' }} + uses: actions/cache/restore@v4.0.2 + with: + path: | + ./**/target/** + .m2/repository/io/helidon/** + enableCrossOsArchive: true + fail-on-cache-miss: true + key: build-cache-${{ github.run_id }}-${{ github.run_attempt }}-${{ inputs.build-cache-id }} + restore-keys: | + build-cache-${{ github.run_id }}-${{ github.run_attempt }}- + build-cache-${{ github.run_id }}- + - name: Exec + env: + MAVEN_ARGS: | + ${{ env.MAVEN_ARGS }} + -Dmaven.repo.local=${{ github.workspace }}/.m2/repository + run: ${{ inputs.run }} + shell: bash + - name: Archive test results + # https://github.com/actions/upload-artifact/issues/240 + if: ${{ inputs.test-artifact-name != '' && runner.os != 'Windows' && always() }} + uses: actions/upload-artifact@v4 + with: + if-no-files-found: 'ignore' + name: ${{ inputs.test-artifact-name }} + path: | + **/target/surefire-reports/** + **/target/failsafe-reports/** + **/target/it/**/*.log + - name: Archive artifacts + if: ${{ inputs.artifact-name != '' && inputs.artifact-path != '' && always() }} + uses: actions/upload-artifact@v4 + with: + if-no-files-found: 'ignore' + name: ${{ inputs.artifact-name }} + path: ${{ inputs.artifact-path }} diff --git a/.github/workflows/assign-issue-to-project.yml b/.github/workflows/assign-issue-to-project.yml deleted file mode 100644 index d639c270277..00000000000 --- a/.github/workflows/assign-issue-to-project.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Assign Issue to Project - -on: - issues: - types: [opened, reopened] -env: - GITHUB_API_KEY: ${{ secrets.GITHUB_TOKEN }} - - -jobs: - Assign-Issue-To-Backlog: - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v4 - - run: etc/scripts/actions/assign-issue-to-project.sh $GITHUB_REPOSITORY ${{ github.event.issue.number }} Backlog Triage diff --git a/.github/workflows/backport-issues.yml b/.github/workflows/backport-issues.yml new file mode 100644 index 00000000000..b017feb9002 --- /dev/null +++ b/.github/workflows/backport-issues.yml @@ -0,0 +1,56 @@ +# +# Copyright (c) 2022, 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Create Backport Issues + +on: + workflow_dispatch: + inputs: + issue: + description: 'Issue number' + required: true + version: + description: 'Helidon version this issue was reported for' + required: true + type: choice + options: + - 2.x + - 3.x + - 4.x + default: '2.x' + target-2: + type: boolean + description: 'Port to 2.x?' + default: false + target-3: + type: boolean + description: 'Port to 3.x?' + default: true + target-4: + type: boolean + description: 'Port to 4.x?' + default: true + +env: + GITHUB_API_KEY: ${{ secrets.GITHUB_TOKEN }} + + +jobs: + Issue-Backport: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - run: etc/scripts/backport-issues.sh $GITHUB_REPOSITORY ${{ github.event.inputs.issue }} ${{ github.event.inputs.version }} ${{ github.event.inputs.target-2 }} ${{ github.event.inputs.target-3 }} ${{ github.event.inputs.target-4 }} diff --git a/.github/workflows/create-backport-issues.yml b/.github/workflows/create-backport-issues.yml deleted file mode 100644 index a42abfdbe49..00000000000 --- a/.github/workflows/create-backport-issues.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Create Backport Issues - -on: - workflow_dispatch: - inputs: - issue: - description: 'Issue number' - required: true - version: - description: 'Helidon version this issue was reported for' - required: true - type: choice - options: - - 2.x - - 3.x - - 4.x - default: '2.x' - target-2: - type: boolean - description: 'Port to 2.x?' - default: false - target-3: - type: boolean - description: 'Port to 3.x?' - default: true - target-4: - type: boolean - description: 'Port to 4.x?' - default: true - -env: - GITHUB_API_KEY: ${{ secrets.GITHUB_TOKEN }} - - -jobs: - Issue-Backport: - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v4 - - run: etc/scripts/actions/create-backport-issues.sh $GITHUB_REPOSITORY ${{ github.event.inputs.issue }} ${{ github.event.inputs.version }} ${{ github.event.inputs.target-2 }} ${{ github.event.inputs.target-3 }} ${{ github.event.inputs.target-4 }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e7e72b18ad3..85d47032d56 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,66 +1,161 @@ -# Notes -# - cannot run on Windows, as we use shell scripts +# +# Copyright (c) 2023, 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. name: "Release" on: push: - branches: - - 'release-*' + branches: [ 'release-*' ] + env: JAVA_VERSION: '21' JAVA_DISTRO: 'oracle' - MAVEN_HTTP_ARGS: '-Dmaven.wagon.httpconnectionManager.ttlSeconds=60 -Dmaven.wagon.http.retryHandler.count=3' + MAVEN_ARGS: | + -B -e + -Dmaven.wagon.httpconnectionManager.ttlSeconds=60 + -Dmaven.wagon.http.retryHandler.count=3 + -Djdk.toolchain.version=${JAVA_VERSION} + -Dcache.enabled=true concurrency: group: release-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - copyright: - timeout-minutes: 10 + create-tag: runs-on: ubuntu-20.04 + environment: release + outputs: + version: ${{ steps.create-tag.outputs.version }} + tag: ${{ steps.create-tag.outputs.tag }} steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Copyright - run: etc/scripts/copyright.sh - release: - timeout-minutes: 60 + fetch-depth: '0' + token: ${{ secrets.SERVICE_ACCOUNT_TOKEN }} + - id: create-tag + run: ./etc/scripts/release.sh create_tag >> "${GITHUB_OUTPUT}" + validate: + needs: create-tag + uses: ./.github/workflows/validate.yml + with: + ref: ${{ needs.create-tag.outputs.tag }} + stage: + needs: [ create-tag, validate ] + strategy: + matrix: + moduleSet: [ core, integrations, others ] runs-on: ubuntu-20.04 + timeout-minutes: 30 environment: release steps: - uses: actions/checkout@v4 with: - token: ${{ secrets.SERVICE_ACCOUNT_TOKEN }} fetch-depth: '0' - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 + ref: ${{ needs.create-tag.outputs.tag }} + - uses: actions/download-artifact@v4 with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Release + pattern: "{javadoc-jars-${{ matrix.moduleSet }},docs}" + merge-multiple: true + - shell: bash env: GPG_PASSPHRASE: ${{ secrets.HELIDON_GPG_PASSPHRASE }} GPG_PRIVATE_KEY: ${{ secrets.HELIDON_GPG_PRIVATE_KEY }} - GPG_PUBLIC_KEY: ${{ secrets.HELIDON_GPG_PUBLIC_KEY }} - MAVEN_SETTINGS: ${{ secrets.MAVEN_SETTINGS }} - RELEASE_WORKFLOW: "true" + run: etc/scripts/setup-gpg.sh + - uses: ./.github/actions/common + with: + build-cache: read-only + artifact-name: io-helidon-artifacts-part-${{ matrix.moduleSet }} + artifact-path: staging + run: | + mvn ${MAVEN_ARGS} \ + -DreactorRule=default \ + -DmoduleSet=${{ matrix.moduleSet }} \ + -Dcache.loadSuffixes=javadoc,docs \ + -Prelease,no-snapshots \ + -DskipTests \ + -DaltDeploymentRepository=":::file://${PWD}/staging" \ + deploy + deploy: + needs: [ create-tag, stage ] + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + ref: ${{ needs.create-tag.outputs.tag }} + - uses: actions/download-artifact@v4 + with: + pattern: io-helidon-artifacts-part-* + path: staging + merge-multiple: true + - shell: bash + env: + NEXUS_USER: ${{ secrets.NEXUS_USER }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} run: | - git config user.email "helidon-robot_ww@oracle.com" - git config user.name "Helidon Robot" - etc/scripts/release.sh release_build - - name: Upload Staged Artifacts - uses: actions/upload-artifact@v4 + etc/scripts/nexus.sh deploy_release \ + --dir="staging" \ + --description="Helidon v%{version}" + - uses: actions/upload-artifact@v4 + with: + name: io-helidon-artifacts-${{ needs.create-tag.outputs.version }} + path: staging + resolve-all: + needs: [ create-tag, deploy ] + timeout-minutes: 30 + runs-on: ubuntu-20.04 + name: resolve-all + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + ref: ${{ needs.create-tag.outputs.tag }} + - uses: ./.github/actions/common + with: + run: | + mvn ${MAVEN_ARGS} -N \ + -Possrh-staging \ + -Dartifact=io.helidon:helidon-all:${{ needs.create-tag.outputs.version }}:pom \ + dependency:get + smoketest: + needs: [ create-tag, deploy ] + timeout-minutes: 30 + strategy: + matrix: + archetype: + - bare-se + - bare-mp + - quickstart-se + - quickstart-mp + - database-se + - database-mp + runs-on: ubuntu-20.04 + name: smoketest/${{ matrix.archetype }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + ref: ${{ needs.create-tag.outputs.tag }} + - uses: ./.github/actions/common with: - name: io-helidon-artifacts-${{ github.ref_name }} - path: parent/target/nexus-staging/ - retention-days: 90 + run: | + ./etc/scripts/smoketest.sh \ + --clean \ + --staged \ + --version=${{ needs.create-tag.outputs.version }} \ + --archetype=${{ matrix.archetype }} diff --git a/.github/workflows/snapshot.yaml b/.github/workflows/snapshot.yaml new file mode 100644 index 00000000000..7cb27e7863d --- /dev/null +++ b/.github/workflows/snapshot.yaml @@ -0,0 +1,114 @@ +# +# Copyright (c) 2023, 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "Snapshot Release" + +on: + workflow_dispatch: + push: + branches: + - 'main' + - 'helidon-*.x' + +env: + JAVA_VERSION: '21' + JAVA_DISTRO: 'oracle' + MAVEN_ARGS: | + -B -e + -Dmaven.wagon.httpconnectionManager.ttlSeconds=60 + -Dmaven.wagon.http.retryHandler.count=3 + -Djdk.toolchain.version=${JAVA_VERSION} + -Dcache.enabled=true + +concurrency: + group: snapshot-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + get-version: + runs-on: ubuntu-20.04 + environment: release + outputs: + version: ${{ steps.get-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + - id: get-version + shell: bash + run: ./etc/scripts/release.sh get_version >> "${GITHUB_OUTPUT}" + validate: + needs: get-version + uses: ./.github/workflows/validate.yml + stage: + needs: [ get-version, validate ] + strategy: + matrix: + moduleSet: [ core, integrations, others ] + runs-on: ubuntu-20.04 + timeout-minutes: 30 + environment: release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + - uses: actions/download-artifact@v4 + with: + pattern: "{javadoc-jars-${{ matrix.moduleSet }},docs}" + merge-multiple: true + - shell: bash + env: + GPG_PASSPHRASE: ${{ secrets.HELIDON_GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.HELIDON_GPG_PRIVATE_KEY }} + run: etc/scripts/setup-gpg.sh + - uses: ./.github/actions/common + with: + build-cache: read-only + artifact-name: io-helidon-artifacts-part-${{ matrix.moduleSet }} + artifact-path: staging + run: | + mvn ${MAVEN_ARGS} \ + -DreactorRule=default \ + -DmoduleSet=${{ matrix.moduleSet }} \ + -Dcache.loadSuffixes=javadoc,docs \ + -Prelease\ + -DskipTests \ + -DaltDeploymentRepository=":::file://${PWD}/staging" \ + deploy + deploy: + needs: [ get-version, stage ] + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + - uses: actions/download-artifact@v4 + with: + pattern: io-helidon-artifacts-part-* + path: staging + merge-multiple: true + - shell: bash + env: + NEXUS_USER: ${{ secrets.NEXUS_USER }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + run: | + etc/scripts/nexus.sh deploy_snapshots \ + --dir="staging" + - uses: actions/upload-artifact@v4 + with: + name: io-helidon-artifacts-${{ needs.get-version.outputs.version }} + path: staging diff --git a/.github/workflows/snapshotrelease.yaml b/.github/workflows/snapshotrelease.yaml deleted file mode 100644 index a4be65e8357..00000000000 --- a/.github/workflows/snapshotrelease.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Perform a snapshot build and deploy to snapshot repository -# Notes -# - cannot run on Windows, as we use shell scripts - -name: "Snapshot Release" - -on: - workflow_dispatch: - -env: - JAVA_VERSION: '21' - JAVA_DISTRO: 'oracle' - MAVEN_HTTP_ARGS: '-Dmaven.wagon.httpconnectionManager.ttlSeconds=60 -Dmaven.wagon.http.retryHandler.count=3' - -concurrency: - group: release-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - deploy: - timeout-minutes: 60 - runs-on: ubuntu-20.04 - environment: release - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Build and deploy - env: - MAVEN_SETTINGS: ${{ secrets.MAVEN_SETTINGS }} - RELEASE_WORKFLOW: "true" - run: | - etc/scripts/release.sh deploy_snapshot diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b67d15807b3..cdf932620ec 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,173 +1,379 @@ -# Notes -# - cannot run on Windows, as we use shell scripts -# - removed macos from most jobs to speed up the process +# +# Copyright (c) 2023, 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. name: "Validate" -on: [pull_request, push] +on: + pull_request: + push: + branches-ignore: [ 'main', 'helidon-*.x', 'release-*' ] + tags-ignore: [ '**' ] + workflow_call: + inputs: + ref: + description: The branch, tag or SHA to checkout + required: false + type: string + default: '' env: - JAVA_VERSION: '21' - JAVA_DISTRO: 'oracle' - HELIDON_PIPELINES: 'true' - MAVEN_HTTP_ARGS: '-Dmaven.wagon.httpconnectionManager.ttlSeconds=60 -Dmaven.wagon.http.retryHandler.count=3' + JAVA_VERSION: 21 + GRAALVM_VERSION: 21.0.3 + JAVA_DISTRO: oracle + MAVEN_ARGS: | + -B -fae -e + -Dmaven.wagon.httpconnectionManager.ttlSeconds=60 + -Dmaven.wagon.http.retryHandler.count=3 + -Djdk.toolchain.version=${JAVA_VERSION} + -Dcache.enabled=true concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: validate-${{ github.ref }} cancel-in-progress: true jobs: copyright: - timeout-minutes: 10 + timeout-minutes: 5 runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Copyright - run: etc/scripts/copyright.sh + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + - uses: ./.github/actions/common + with: + run: etc/scripts/copyright.sh checkstyle: - timeout-minutes: 10 + timeout-minutes: 5 runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Checkstyle - run: etc/scripts/checkstyle.sh - spotbugs: - timeout-minutes: 45 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + run: etc/scripts/checkstyle.sh + shellcheck: + timeout-minutes: 5 runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Spotbugs - run: etc/scripts/spotbugs.sh - docs: - timeout-minutes: 30 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + maven-cache: none + run: etc/scripts/shellcheck.sh + build: + timeout-minutes: 15 runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Docs - run: etc/scripts/docs.sh - build: - timeout-minutes: 60 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-write + maven-cache: read-write + run: | + mvn ${MAVEN_ARGS} build-cache:go-offline + mvn ${MAVEN_ARGS} -T8 \ + -Dorg.slf4j.simpleLogger.showThreadName=true \ + -DskipTests \ + -Ptests \ + install + _tests: + needs: build + timeout-minutes: 30 strategy: matrix: os: [ ubuntu-20.04 ] + moduleSet: [ core, it, dbclient, dbclient-oracle, others ] + include: + - { os: ubuntu-20.04, platform: linux } runs-on: ${{ matrix.os }} + name: tests/${{ matrix.moduleSet }} steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Maven build - run: etc/scripts/github-build.sh - examples: - timeout-minutes: 40 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-only + test-artifact-name: tests-${{ matrix.moduleSet }} + run: | + mvn ${MAVEN_ARGS} \ + -DreactorRule=tests \ + -DmoduleSet=${{ matrix.moduleSet }} \ + -Dsurefire.reportNameSuffix=${{ matrix.platform }} \ + verify + _tck: + needs: build + timeout-minutes: 30 + strategy: + matrix: + moduleSet: [ cdi, rest, others ] + runs-on: ubuntu-20.04 + name: tests/tck-${{ matrix.moduleSet }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-only + test-artifact-name: tests-tck-${{ matrix.moduleSet }} + run: | + mvn ${MAVEN_ARGS} \ + -DreactorRule=tck \ + -DmoduleSet=${{ matrix.moduleSet }} \ + verify + _spotbugs: + needs: build + timeout-minutes: 30 + strategy: + matrix: + moduleSet: [ core, integrations, others ] + runs-on: ubuntu-20.04 + name: spotbugs/${{ matrix.moduleSet }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-only + run: | + mvn ${MAVEN_ARGS} -T8 \ + -Dorg.slf4j.simpleLogger.showThreadName=true \ + -DreactorRule=default \ + -DmoduleSet=${{ matrix.moduleSet }} \ + -DskipTests \ + -Pspotbugs \ + verify + javadoc: + needs: build + timeout-minutes: 30 + strategy: + matrix: + moduleSet: [ core, integrations, others ] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-only + artifact-name: javadoc-jars-${{ matrix.moduleSet }} + artifact-path: | + **/target/state-javadoc.xml + **/target/*-javadoc.jar + run: | + mvn ${MAVEN_ARGS} -T8 \ + -Dorg.slf4j.simpleLogger.showThreadName=true \ + -DreactorRule=default \ + -DmoduleSet=${{ matrix.moduleSet }} \ + -Dcache.recordSuffix=javadoc \ + -DskipTests \ + -Pjavadoc \ + package + docs: + needs: build + timeout-minutes: 15 + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-only + artifact-name: docs + artifact-path: | + */target/state-docs.xml + */target/*-docs.jar + run: | + mvn ${MAVEN_ARGS} \ + -Dcache.recordSuffix=docs \ + -f docs/pom.xml \ + -Pjavadoc \ + install + quickstarts: + needs: build + timeout-minutes: 30 strategy: matrix: os: [ ubuntu-20.04, macos-14 ] + include: + - { os: ubuntu-20.04, platform: linux } + - { os: macos-14, platform: macos } runs-on: ${{ matrix.os }} + name: quickstarts/${{ matrix.platform }} steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - uses: graalvm/setup-graalvm@v1 - with: - java-version: 21 - distribution: graalvm-community - github-token: ${{ secrets.GITHUB_TOKEN }} - native-image-job-reports: true - cache: maven - - name: Maven build - run: | - mvn -B -e "-Dmaven.test.skip=true" $MAVEN_HTTP_ARGS -DskipTests -Ppipeline install - cd examples - mvn -B verify - - name: Test quickstarts native image - run: etc/scripts/test-quickstarts.sh - mp-tck: - timeout-minutes: 60 - name: "MicroProfile TCKs" + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + free-space: true + build-cache: read-only + native-image: true + test-artifact-name: tests-quickstarts-${{ matrix.platform }} + run: | + etc/scripts/test-quickstarts.sh + examples: + needs: build + timeout-minutes: 30 strategy: matrix: - os: [ ubuntu-20.04 ] + os: [ ubuntu-20.04, macos-14 ] + include: + - { os: ubuntu-20.04, platform: linux } + - { os: macos-14, platform: macos } runs-on: ${{ matrix.os }} + name: examples/${{ matrix.platform }} steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Maven build - run: etc/scripts/mp-tck.sh + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + free-space: true + build-cache: read-only + test-artifact-name: tests-examples-${{ matrix.platform }} + run: etc/scripts/build-examples.sh archetypes: - timeout-minutes: 45 + needs: build + timeout-minutes: 30 strategy: matrix: - os: [ ubuntu-20.04 ] - runs-on: ${{ matrix.os }} + group: [ r1, r2, r3, r4, r5 ] + packaging: [ jar ] + include: + - { group: r1, start: 1, end: 25 } + - { group: r2, start: 26, end: 50 } + - { group: r3, start: 51, end: 75 } + - { group: r4, start: 75, end: 100 } + - { group: r5, start: 101, end: -1 } + - { packaging: jar } + runs-on: ubuntu-20.04 + name: archetypes/${{ matrix.group }}-${{ matrix.packaging }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-only + native-image: ${{ matrix.packaging == 'native' }} + test-artifact-name: tests-archetypes-${{ matrix.group }}-${{ matrix.packaging }} + run: | + mvn ${MAVEN_ARGS} \ + -f archetypes/archetypes/pom.xml \ + -Darchetype.test.permutationStartIndex=${{ matrix.start }} \ + -Darchetype.test.permutationEndIndex=${{ matrix.end }} \ + -Darchetype.test.testGoal=verify \ + -Darchetype.test.testProfiles=${{ matrix.profile }} \ + verify + legacy-archetypes: + needs: build + timeout-minutes: 30 + runs-on: ubuntu-20.04 + name: archetypes/legacy steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4.1.0 - with: - distribution: ${{ env.JAVA_DISTRO }} - java-version: ${{ env.JAVA_VERSION }} - cache: maven - - name: Test archetypes - run: etc/scripts/test-archetypes.sh + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + build-cache: read-only + test-artifact-name: tests-legacy-archetypes + run: | + mvn ${MAVEN_ARGS} \ + -f archetypes/pom.xml \ + -Darchetype.test.generatePermutations=false \ + install packaging: - timeout-minutes: 60 + needs: build + timeout-minutes: 30 + strategy: + matrix: + os: [ ubuntu-20.04, macos-14 ] + packaging: [ jar, jlink ] + include: + - { os: ubuntu-20.04, platform: linux } + - { os: macos-14, platform: macos } + runs-on: ${{ matrix.os }} + name: tests/packaging-${{ matrix.packaging }}-${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + free-space: true + build-cache: read-only + test-artifact-name: tests-packaging-${{ matrix.packaging }}-${{ matrix.platform }} + run: | + mvn ${MAVEN_ARGS} \ + -f tests/integration/packaging/pom.xml \ + -P${{ matrix.packaging }}-image \ + verify + _native-image: + needs: build + timeout-minutes: 30 strategy: matrix: - os: [ ubuntu-20.04, macos-14] + os: [ ubuntu-20.04, macos-14 ] + module: [ mp-1, mp-2, mp-3, se-1, inject ] + include: + - { os: ubuntu-20.04, platform: linux } + - { os: macos-14, platform: macos } runs-on: ${{ matrix.os }} + name: tests/native-image-${{ matrix.module }}-${{ matrix.platform }} steps: - uses: actions/checkout@v4 - - uses: graalvm/setup-graalvm@v1 - with: - java-version: 21 - distribution: graalvm-community - github-token: ${{ secrets.GITHUB_TOKEN }} - native-image-job-reports: true - cache: maven - - name: Build Helidon - run: etc/scripts/github-compile.sh - - name: JAR packaging - run: etc/scripts/test-packaging-jar.sh - - name: JLink packaging - run: etc/scripts/test-packaging-jlink.sh - - name: Native-Image packaging - run: etc/scripts/test-packaging-native.sh + with: + ref: ${{ inputs.ref }} + - uses: ./.github/actions/common + with: + free-space: true + build-cache: read-only + native-image: true + test-artifact-name: tests-native-image-${{ matrix.module }}-${{ matrix.platform }} + run: | + mvn ${MAVEN_ARGS} \ + -f tests/integration/packaging/pom.xml \ + -pl ${{ matrix.module }} \ + -Pnative-image \ + -am \ + verify + test-results: + runs-on: ubuntu-20.04 + needs: [ _tests, archetypes, legacy-archetypes, _tck, packaging, _native-image ] + name: tests/results + steps: + - uses: actions/upload-artifact/merge@v4 + with: + name: test-results + pattern: "tests-*" + gate: + runs-on: ubuntu-20.04 + needs: [ copyright, checkstyle, shellcheck, docs, javadoc, _spotbugs, test-results ] + steps: + - shell: bash + run: | + echo OK diff --git a/.gitignore b/.gitignore index b1bf6a636d4..c99b9bcf33b 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,9 @@ node/ # Helidon CLI .helidon +# Helidon examples repository +helidon-examples + # Other *~ user.txt diff --git a/.mvn/cache-config.xml b/.mvn/cache-config.xml new file mode 100644 index 00000000000..2ecbfd32c26 --- /dev/null +++ b/.mvn/cache-config.xml @@ -0,0 +1,140 @@ + + + + false + + + + src/main/asciidoc/config/* + + + + + .*/** + etc/** + + + + + *@copy-libs + + + + + + tests + + + + + webserver/** + webclient/** + common/** + config/** + security/** + + + + + tests/integration/jpa/oracle/** + + + + + tests/integration/jpa/** + + + + + tests/integration/dbclient/oracle/** + + + + + tests/integration/dbclient/** + + + + + tests/integration/packaging/** + + + + + tests/integration/** + + + + + **/* + + + tests/benchmark/** + + + + + + + tests + tck + + + + + microprofile/tests/tck/tck-cdi*/** + + + + + microprofile/tests/tck/tck-rest*/** + + + + + microprofile/tests/tck/** + + + + + + + + + webserver/** + webclient/** + common/** + config/** + security/** + + + + + integrations/** + + + + + **/* + + + + + + diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 00000000000..ad9cb3a7b00 --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,27 @@ + + + + + io.helidon.build-tools + helidon-build-cache-maven-extension + 4.0.12 + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 982e337990e..6487d5857df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,248 @@ For Helidon 2.x releases please see [Helidon 2.x CHANGELOG.md](https://github.co For Helidon 1.x releases please see [Helidon 1.x CHANGELOG.md](https://github.com/oracle/helidon/blob/helidon-1.x/CHANGELOG.md) +## [4.1.2] + +This release contains important bugfixes and enhancements and is recommended for all users of Helidon 4. + +A minimum of Java 21 is required to use Helidon 4. + +### CHANGES + +- gRPC: Adds support to iterate over URIs when connecting to a gRPC service [9300](https://github.com/helidon-io/helidon/pull/9300) +- LRA: LRA testing feature [9320](https://github.com/helidon-io/helidon/pull/9320) +- Logging: JSON Formatter for JUL [9301](https://github.com/helidon-io/helidon/pull/9301) +- Security: Policy validator configurable per endpoint in config (#9248) [9308](https://github.com/helidon-io/helidon/pull/9308) +- WebServer: Allows the webserver's write buffer size to be set to 0 [9314](https://github.com/helidon-io/helidon/pull/9314) +- WebServer: Fix DataReader.findNewLine with lone EOL character [9327](https://github.com/helidon-io/helidon/pull/9327) +- WebServer: Grouping Executors related methods into a single class [9298](https://github.com/helidon-io/helidon/pull/9298) +- WebServer: New implementation for SSE in webserver [9297](https://github.com/helidon-io/helidon/pull/9297) +- WebServer: Smart async writer in webserver [9292](https://github.com/helidon-io/helidon/pull/9292) +- Dependencies: Upgrade Jersey to 3.1.8 [9303](https://github.com/helidon-io/helidon/pull/9303) +- Dependencies: Upgrades protobuf to 3.25.5 [9299](https://github.com/helidon-io/helidon/pull/9299) +- Dependencies: Uptake build-tools 4.0.12 (fixes [9305](https://github.com/helidon-io/helidon/issues/9305)) [9323](https://github.com/helidon-io/helidon/pull/9323) +- Docs: Add emphasis on including an OTel exporter and configuring [9312](https://github.com/helidon-io/helidon/pull/9312) +- Docs: Document work-around for maven archetype issue (#9316) [9324](https://github.com/helidon-io/helidon/pull/9324) +- Tests: Fix DbClient PostgreSQL tests [9293](https://github.com/helidon-io/helidon/pull/9293) + +## [4.1.1] + +This release contains important bugfixes and enhancements and is recommended for all users of Helidon 4. It is compatible with Helidon 4.0.X. + +A minimum of Java 21 is required to use Helidon 4. + +### Notable Changes + +- Implement gRPC MP Client [9026](https://github.com/helidon-io/helidon/pull/9026) + +### CHANGES + +- CORS: Remove headers that do not affect CORS decision-making from request adapter logging output [9178](https://github.com/helidon-io/helidon/pull/9178) +- Codegen: Add support for additional modifiers [9201](https://github.com/helidon-io/helidon/pull/9201) +- Codegen: Fix generation of annotations, including lists, nested annotations etc. [9182](https://github.com/helidon-io/helidon/pull/9182) +- Codegen: Handling enum and type values in a consistent way in code generation. [9167](https://github.com/helidon-io/helidon/pull/9167) +- Codegen: Support for validation of Duration and URI default values. [9166](https://github.com/helidon-io/helidon/pull/9166) +- Codegen: Udpates to types and annotation processing [9168](https://github.com/helidon-io/helidon/pull/9168) +- Config: Replace manual casts on pattern with instanceof in HoconConfigParser [9209](https://github.com/helidon-io/helidon/pull/9209) +- LRA: Replace deprecated method Scheduling.fixedRateBuilder() [9098](https://github.com/helidon-io/helidon/pull/9098) +- Security: Required authorization propagated from the class level now [9137](https://github.com/helidon-io/helidon/pull/9137) +- Tracing: Allow users to direct Helidon to use an existing global `OpenTelemetry` instance rather than create its own [9205](https://github.com/helidon-io/helidon/pull/9205) +- WebServer: Allows the creation of empty SSE events [9207](https://github.com/helidon-io/helidon/pull/9207) +- WebServer: Increases default value of write-buffer-size to 4K [9190](https://github.com/helidon-io/helidon/pull/9190) +- WebServer: UncheckedIOException no longer a special case [9206](https://github.com/helidon-io/helidon/pull/9206) +- gRPC: Downgrades version of protobuf for backwards compatibility [9162](https://github.com/helidon-io/helidon/pull/9162) +- gRPC: Implements support for client gRPC channel injections [9155](https://github.com/helidon-io/helidon/pull/9155) +- gRPC: Implements the gRPC MP Client API [9026](https://github.com/helidon-io/helidon/pull/9026) +- gRPC: Renames package-private Grpc type to GrpcRouteHandler [9173](https://github.com/helidon-io/helidon/pull/9173) +- Build: Fix nightly script [9221](https://github.com/helidon-io/helidon/pull/9221) +- Build: Update release workflows [9210](https://github.com/helidon-io/helidon/pull/9210) +- Dependencies: Upgrade microprofile-cdi-tck to 4.0.13 [9141](https://github.com/helidon-io/helidon/pull/9141) +- Dependencies: Upgrade oci sdk to 3.46.1 [9179](https://github.com/helidon-io/helidon/pull/9179) +- Dependencies: Upgrade slf4j to 2.0.16 [9143](https://github.com/helidon-io/helidon/pull/9143) +- Deprecation: deprecate old injection integration for oci [9184](https://github.com/helidon-io/helidon/pull/9184) +- Docs: Clarify description of config profiles [9188](https://github.com/helidon-io/helidon/pull/9188) +- Docs: Documents gRPC MP Client API [9150](https://github.com/helidon-io/helidon/pull/9150) +- Tests: Builder tests that confidential options are not printed in toString() [9154](https://github.com/helidon-io/helidon/pull/9154) + +## [4.1.0] + +This release contains important bugfixes and enhancements and is recommended for all users of Helidon 4. It is compatible with Helidon 4.0.X. + +A minimum of Java 21 is required to use Helidon 4. + +### Notable Changes + +- Support for MicroProfile 6.1 [8704](https://github.com/helidon-io/helidon/issues/8704) +- gRPC support [5418](https://github.com/helidon-io/helidon/issues/5418) +- Support for Java 22 and Java 23 + +### CHANGES + +- Builders: Fixed configuration metadata of blueprints that are configured and provide a service [8891](https://github.com/helidon-io/helidon/pull/8891) +- Common: Convert `ConcurrentHashMap` which does service loading to `HashMap` with `ReentrantLock` [8977](https://github.com/helidon-io/helidon/pull/8977) +- Common: Fix SetCookie to work for client side as well [9029](https://github.com/helidon-io/helidon/pull/9029) +- Common: Improved parsing of HTTP/1 prologue and headers. [8890](https://github.com/helidon-io/helidon/pull/8890) +- Common: Introduction of HSON library to write and parse Helidon metadata files. [9050](https://github.com/helidon-io/helidon/pull/9050) +- Common: Mapper manager cache key fix [9121](https://github.com/helidon-io/helidon/pull/9121) +- Common: Methods to retrieve optional typed entity [8939](https://github.com/helidon-io/helidon/pull/8939) +- Common: Remove unused parameters from JsonpWriter [8979](https://github.com/helidon-io/helidon/pull/8979) +- Common: Replace deprecated method Header.value() on Header.get() [8873](https://github.com/helidon-io/helidon/pull/8873) +- Common: Update UriEncoding.decode to expose a decodeQuery method [9006](https://github.com/helidon-io/helidon/pull/9006) +- Common: Use Helidon metadata format (HSON) for service registry generated file. [9061](https://github.com/helidon-io/helidon/pull/9061) +- Common: Use Hson.Struct instead of Hson.Object to prevent confusion with java.lang.Object [9080](https://github.com/helidon-io/helidon/pull/9080) +- Common: Use System.Logger instead of JUL where applicable #7792 [8791](https://github.com/helidon-io/helidon/pull/8791) +- Common: Use string constructor of BigDecimal to avoid bad decimals in output. [9074](https://github.com/helidon-io/helidon/pull/9074) +- Config: Upgrade to MP Config 3.1 and fix an issue with profile specific properties [8757](https://github.com/helidon-io/helidon/pull/8757) +- DBClient: Add uses io.helidon.dbclient.jdbc.spi.JdbcConnectionPoolProvider to io.helidon.dbclient.jdbc module (#8237) [8850](https://github.com/helidon-io/helidon/pull/8850) +- DBClient: Consider missing named parameters values in the parameters Map as null [9035](https://github.com/helidon-io/helidon/pull/9035) +- DBClient: Fix DbClientService for Mongo DbClient [9102](https://github.com/helidon-io/helidon/pull/9102) +- FT: Fix confusing log message on breach of overallTimeout duration [8936](https://github.com/helidon-io/helidon/pull/8936) +- FT: Remove unused constructor parameter from io.helidon.faulttolerance.AsyncImpl [9020](https://github.com/helidon-io/helidon/pull/9020) +- FT: Use correct exception when retrying in FT [8983](https://github.com/helidon-io/helidon/pull/8983) +- gRPC: Client Implementation [8423](https://github.com/helidon-io/helidon/pull/8423) +- gRPC: MP Implementation [8878](https://github.com/helidon-io/helidon/pull/8878) +- JEP290: forward port of serial-config fix [8814](https://github.com/helidon-io/helidon/pull/8814) +- JTA: Refactors JtaConnection to allow status enforcement by JTA implementation [8479](https://github.com/helidon-io/helidon/pull/8479) +- JTA: Removes usage of ConcurrentHashMap in LocalXAResource.java to avoid thread pinning in JDKs of version 22 and lower [8900](https://github.com/helidon-io/helidon/pull/8900) +- Logging: Bugfixes log builder [9051](https://github.com/helidon-io/helidon/pull/9051) +- MDC: propagation without context [8957](https://github.com/helidon-io/helidon/pull/8957) +- Metrics: Add RW locking to better manage concurrency [8997](https://github.com/helidon-io/helidon/pull/8997) +- Metrics: Add deprecation logging and mention in Micrometer integration doc pages [9100](https://github.com/helidon-io/helidon/pull/9100) +- Metrics: MP Metrics 5.1 support [9032](https://github.com/helidon-io/helidon/pull/9032) +- Metrics: Mark deprecations for Micrometer integration component [9085](https://github.com/helidon-io/helidon/pull/9085) +- Metrics: Properly handle disabled metrics in MP [8908](https://github.com/helidon-io/helidon/pull/8908) +- Metrics: Update metrics config default for `rest-request-enabled` and add doc text explaining SE vs. MP defaults for some values [8912](https://github.com/helidon-io/helidon/pull/8912) +- Native image fixes (required for Java 22) [9028](https://github.com/helidon-io/helidon/pull/9028) +- Native image: Add required reflection configuration for EclipseLink [8871](https://github.com/helidon-io/helidon/pull/8871) +- Native image: to support latest dev release of GraalVM native image [8838](https://github.com/helidon-io/helidon/pull/8838) +- OCI: Add Imds data retriever as a service provider [8928](https://github.com/helidon-io/helidon/pull/8928) +- OCI: Oci integration fixes [8927](https://github.com/helidon-io/helidon/pull/8927) +- OCI: Service registry OCI integration update [8921](https://github.com/helidon-io/helidon/pull/8921) +- OCI: Support for OKE Workload identity in OCI integration for Service registry [8862](https://github.com/helidon-io/helidon/pull/8862) +- OCI: Update oci.auth-strategy values in generated OCI archetype to avoid UnsatisfiedResolutionException [9073](https://github.com/helidon-io/helidon/pull/9073) +- SSE mediaType comes null after first consumed event [8922](https://github.com/helidon-io/helidon/pull/8922) +- Security: ConcurrentHashMap guarding added [9114](https://github.com/helidon-io/helidon/pull/9114) +- Security: Correctly guard concurrent access to hash map [9031](https://github.com/helidon-io/helidon/pull/9031) +- Security: Fixed concurrent access to identity hash map with reentrant lock. [9030](https://github.com/helidon-io/helidon/pull/9030) +- Security: Jwt improvements [8865](https://github.com/helidon-io/helidon/pull/8865) +- Service Registry [8766](https://github.com/helidon-io/helidon/pull/8766) +- Tracing: Adopt MP Telemetry 1.1 [8984](https://github.com/helidon-io/helidon/pull/8984) +- Tracing: After retrieval check baggage entry for null before dereferencing it [8885](https://github.com/helidon-io/helidon/pull/8885) +- Tracing: Fix tracer information propagation across threads using Helidon context [8841](https://github.com/helidon-io/helidon/pull/8841) +- Tracing: Reorder checking of delegate vs. wrapper in OTel tracer unwrap [8855](https://github.com/helidon-io/helidon/pull/8855) +- Tracing: Replace deprecated method Span.baggage(key) on Span.baggage().get(key) [9042](https://github.com/helidon-io/helidon/pull/9042) +- WebClient: Attempt to read an unconsumed response entity to allow connection caching [8943](https://github.com/helidon-io/helidon/pull/8943) +- WebClient: Client connection properly returned to the cache [9115](https://github.com/helidon-io/helidon/pull/9115) +- WebClient: Fix multi-value query string parsing [8889](https://github.com/helidon-io/helidon/pull/8889) +- WebClient: Moves client protocol ID caching from HttpClientRequest to WebClient [8933](https://github.com/helidon-io/helidon/pull/8933) +- WebClient: Remove unnecessary field length from ContentLengthInputStream [8915](https://github.com/helidon-io/helidon/pull/8915) +- WebClient: not routing the requests through proxy configured using Proxy Builder. #9022 [9023](https://github.com/helidon-io/helidon/pull/9023) +- WebServer: Avoids running the encoders (such as GZIP) when no data is written [9117](https://github.com/helidon-io/helidon/pull/9117) +- WebServer: Fix problem where throwing an Error would close connection but send keep-alive [9014](https://github.com/helidon-io/helidon/pull/9014) +- WebServer: HTTP2-Settings needs to be encoded/decoded to Base64 with url dialect [8845](https://github.com/helidon-io/helidon/pull/8845) +- WebServer: Replaces ConcurrentHashMap to avoid potential thread pinning [8995](https://github.com/helidon-io/helidon/pull/8995) +- WebServer: Retrieve the correct requested URI info path value, indpt of the routing path used to locate the handler [8823](https://github.com/helidon-io/helidon/pull/8823) +- WebServer: Return correct status on too long prologue [9001](https://github.com/helidon-io/helidon/pull/9001) +- WebServer: Server TLS - Add path key description [8937](https://github.com/helidon-io/helidon/pull/8937) +- WebServer: Skips content encoding of empty entities [9000](https://github.com/helidon-io/helidon/pull/9000) +- WebServer: Update max-prologue-length from 2048 to 4096 to align with 3.x [9007](https://github.com/helidon-io/helidon/pull/9007) +- WebServer: improvement of header parsing error handling [8831](https://github.com/helidon-io/helidon/pull/8831) +- WebServer: register routing in weighted order of Server and HTTP Features [8826](https://github.com/helidon-io/helidon/pull/8826) +- WebSocket: Makes SocketContext available to a WsSession [8944](https://github.com/helidon-io/helidon/pull/8944) +- Archetype: Remove unused config property from generated code [8965](https://github.com/helidon-io/helidon/pull/8965) +- Archetype: fix Native image build for `quickstart` with `jackson` [8835](https://github.com/helidon-io/helidon/pull/8835) +- Archetype: fix database app-type typo [8963](https://github.com/helidon-io/helidon/pull/8963) +- Build: Add post pr merge workflow to support continuous snapshot deployments [8919](https://github.com/helidon-io/helidon/pull/8919) [8924](https://github.com/helidon-io/helidon/pull/8924) [8923](https://github.com/helidon-io/helidon/pull/8923) +- Build: Cleanup validate workflow [9108](https://github.com/helidon-io/helidon/pull/9108) +- Build: Fix release.sh [9087](https://github.com/helidon-io/helidon/pull/9087) +- Build: POM cleanups [9110](https://github.com/helidon-io/helidon/pull/9110) +- Build: Parallelized pipelines [9111](https://github.com/helidon-io/helidon/pull/9111) +- Build: ShellCheck [9078](https://github.com/helidon-io/helidon/pull/9078) +- Build: Uptake Helidon Build Tools v4.0.9 [9086](https://github.com/helidon-io/helidon/pull/9086) +- Dependencies: Bump up cron-utils [9120](https://github.com/helidon-io/helidon/pull/9120) +- Dependencies: GraphQL upgrade [9109](https://github.com/helidon-io/helidon/pull/9109) +- Dependencies: Java 22 support. Upgrade ASM, byte-buddy, and eclipselink [8956](https://github.com/helidon-io/helidon/pull/8956) +- Dependencies: Update eclipselink to 4.0.4 [9015](https://github.com/helidon-io/helidon/pull/9015) +- Dependencies: Upgrade oci-sdk to 3.45.0 [9083](https://github.com/helidon-io/helidon/pull/9083) +- Dependencies: Upgrade snakeyaml to 2.2 [9072](https://github.com/helidon-io/helidon/pull/9072) +- Dependencies: Upgrades gRPC dependencies to latest versions [9105](https://github.com/helidon-io/helidon/pull/9105) +- Dependencies: jakarta ee upgrades [9089](https://github.com/helidon-io/helidon/pull/9089) +- Docs: Add back and enhance the page describing OpenAPI generation for Helidon 4 [9052](https://github.com/helidon-io/helidon/pull/9052) +- Docs: Clarify javadoc for HealthCheckResponse.Builder.status(boolean) [9043](https://github.com/helidon-io/helidon/pull/9043) +- Docs: Cleanup prerequisites and use of prereq table [9063](https://github.com/helidon-io/helidon/pull/9063) +- Docs: Config reference documentation [9053](https://github.com/helidon-io/helidon/pull/9053) +- Docs: Correct the ordering of whenSent in doc snippet [8884](https://github.com/helidon-io/helidon/pull/8884) +- Docs: Doc for @AddConfigBlock #8807 [8825](https://github.com/helidon-io/helidon/pull/8825) +- Docs: Document supported GraalVM version for native-image [8938](https://github.com/helidon-io/helidon/pull/8938) +- Docs: Documents the gRPC MP server API [9123](https://github.com/helidon-io/helidon/pull/9123) +- Docs: Excluding generated service descriptors from javadoc plugin(s). [9082](https://github.com/helidon-io/helidon/pull/9082) +- Docs: Generate config docs during build [9103](https://github.com/helidon-io/helidon/pull/9103) +- Docs: Mocking documentation [8787](https://github.com/helidon-io/helidon/pull/8787) +- Docs: Update Keycloak version to 24 in OIDC guide [8868](https://github.com/helidon-io/helidon/pull/8868) +- Docs: Update generated config reference [8852](https://github.com/helidon-io/helidon/pull/8852) +- Docs: Update microprofile spec versions in docs [9095](https://github.com/helidon-io/helidon/pull/9095) +- Docs: Updates links to examples that are in documentation to point to the `helidon-examples` repository. [9094](https://github.com/helidon-io/helidon/pull/9094) +- Examples: Fix example to use the configured values. [8994](https://github.com/helidon-io/helidon/pull/8994) +- Examples: Skip test if InstancePrincipal UT if Imds is available [8985](https://github.com/helidon-io/helidon/pull/8985) +- Examples: Updates versions of beans.xml resources to 4.0 [9038](https://github.com/helidon-io/helidon/pull/9038) +- Examples: examples removal [9034](https://github.com/helidon-io/helidon/pull/9034) +- Test: add helidon-logging-jul as a test dependency to some modules #779 [8810](https://github.com/helidon-io/helidon/pull/8810) +- Test: Add `classesDirectory` configuration to failsafe plugin [9059](https://github.com/helidon-io/helidon/pull/9059) +- Test: DbClient IT tests job [9107](https://github.com/helidon-io/helidon/pull/9107) +- Test: Packaging Integration Tests [9106](https://github.com/helidon-io/helidon/pull/9106) +- Test: Re-add tck-fault-tolerance module in the reactor [9112](https://github.com/helidon-io/helidon/pull/9112) +- Test: Reenables failing JPA test [9037](https://github.com/helidon-io/helidon/pull/9037) +- Test: Refactor DbClient integration tests [9104](https://github.com/helidon-io/helidon/pull/9104) +- Test: Restored test TenantTest#test2 after changes in FT [8832](https://github.com/helidon-io/helidon/pull/8832) +- Test: Update microprofile tck artifact install [9077](https://github.com/helidon-io/helidon/pull/9077) +- Test: Use Hamcrest assertions instead of JUnit in common/buffers (#1749) [8883](https://github.com/helidon-io/helidon/pull/8883) +- Test: Use Hamcrest assertions instead of JUnit in dbclient/mongodb (#1749) [8934](https://github.com/helidon-io/helidon/pull/8934) +- Test: Use Hamcrest assertions instead of JUnit in webclient/http1 (#1749) [8914](https://github.com/helidon-io/helidon/pull/8914) + + +## [4.0.11] + +This release contains important bugfixes and is recommended for all users of Helidon 4. + +Java 21 is required to use Helidon 4 + +### CHANGES + +- Common: Update UriEncoding.decode to expose a decodeQuery method [9009](https://github.com/helidon-io/helidon/pull/9009) +- JTA: Removes usage of ConcurrentHashMap in LocalXAResource.java [8988](https://github.com/helidon-io/helidon/pull/8988) +- Metrics: Add RW locking to better manage concurrency [8999](https://github.com/helidon-io/helidon/pull/8999) +- Metrics: Properly handle disabled metrics in MP [8976](https://github.com/helidon-io/helidon/pull/8976) +- Observability: Convert `ConcurrentHashMap` which does service loading to `HashMap` with reentrant lock [8991](https://github.com/helidon-io/helidon/pull/8991) +- Tracing: After retrieval check baggage entry for null before dereferencing it [8975](https://github.com/helidon-io/helidon/pull/8975) +- WebClient: Attempt to read an unconsumed response entity to allow connection caching [8996](https://github.com/helidon-io/helidon/pull/8996) +- WebClient: Moves client protocol ID caching from HttpClientRequest to WebClient [8987](https://github.com/helidon-io/helidon/pull/8987) +- WebServer: Fix problem where throwing an Error would close connection but send keep-alive [9016](https://github.com/helidon-io/helidon/pull/9016) +- WebServer: Skips content encoding of empty entities. [9008](https://github.com/helidon-io/helidon/pull/9008) +- WebServer: Update max-prologue-length from 2048 to 4096 to align with 3.x [9010](https://github.com/helidon-io/helidon/pull/9010) +- Dependencies: Update eclipselink to 4.0.4 [9017](https://github.com/helidon-io/helidon/pull/9017) +- Dependencies: Upgrade oci-sdk to 3.43.2 [8961](https://github.com/helidon-io/helidon/pull/8961) +- Examples: Archetype: Remove unused config property from generated code [8990](https://github.com/helidon-io/helidon/pull/8990) +- Examples: Archetype: fix database app-type typo (#8963) [8989](https://github.com/helidon-io/helidon/pull/8989) +- Testing: Skip test if InstancePrincipal UT if Imds is available [8992](https://github.com/helidon-io/helidon/pull/8992) + +## [4.0.10] + +This release contains important bugfixes and enhancements and is recommended for all users of Helidon 4. + +Java 21 is required to use Helidon 4.0.10. + +### CHANGES + +- Fault Tolerance: implement a new method caching strategy in fault tolerance. [8842](https://github.com/helidon-io/helidon/pull/8842) +- Tracing: Reorder checking of delegate vs. wrapper in OTel tracer unwrap ( [8859](https://github.com/helidon-io/helidon/pull/8859) +- Tracing: tracer information propagation across threads using Helidon context [8847](https://github.com/helidon-io/helidon/pull/8847) +- WebServer: HTTP2-Settings needs to be encoded/decoded to Base64 with url dialect [8853](https://github.com/helidon-io/helidon/pull/8853) +- WebServer: Fix handling of invalid end of line in HTTP header parsing. Added tests [8843](https://github.com/helidon-io/helidon/pull/8843) +- WebServer: Retrieve the correct requested URI info path value, indpt of the routing path used to locate the handler [8844](https://github.com/helidon-io/helidon/pull/8844) +- WebServer: register routing in weighted order of Server and HTTP Features [8840](https://github.com/helidon-io/helidon/pull/8840) +- Native Image: Updates to support latest dev release of GraalVM native image [8838](https://github.com/helidon-io/helidon/pull/8838) +- Security: JWT improvements [8865](https://github.com/helidon-io/helidon/pull/8865) + ## [4.0.9] This release contains important bugfixes and ehancements and is recommended for all users of Helidon 4. @@ -1217,6 +1459,11 @@ Helidon 4.0.0 is a major release that includes significant new features and fixe - MicroProfile: MP path based static content should use index.html (4.x) [4737](https://github.com/oracle/helidon/pull/4737) - Build: 4.0 version and poms [4655](https://github.com/oracle/helidon/pull/4655) +[4.1.2]: https://github.com/oracle/helidon/compare/4.1.1...4.1.2 +[4.1.1]: https://github.com/oracle/helidon/compare/4.1.0...4.1.1 +[4.1.0]: https://github.com/oracle/helidon/compare/4.0.11...4.1.0 +[4.0.11]: https://github.com/oracle/helidon/compare/4.0.10...4.0.11 +[4.0.10]: https://github.com/oracle/helidon/compare/4.0.9...4.0.10 [4.0.9]: https://github.com/oracle/helidon/compare/4.0.8...4.0.9 [4.0.8]: https://github.com/oracle/helidon/compare/4.0.7...4.0.8 [4.0.7]: https://github.com/oracle/helidon/compare/4.0.6...4.0.7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8409097403d..c1f4b46a8c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ We welcome your contributions! There are multiple ways to contribute. Join us at [#helidon-users](http://slack.helidon.io) and participate in discussions. -## Issues +## Opening Issues If you hit a bug or have an enhancement request then file a [GitHub issue](https://github.com/oracle/helidon/issues). When filing a bug remember that the better written the bug is, the more likely it is @@ -19,20 +19,22 @@ to be fixed. Please include: 4. Version of Docker or Kubernetes or other software if it's relevant to your issue 5. Steps to reproduce -## Code +## Contributing Code We welcome code contributions, but we need the contributor to sign the [Oracle Contributor Agreement (OCA)](https://oca.opensource.oracle.com) first. -The process: +## Pull Request Process 0. Sign the [OCA](https://oca.opensource.oracle.com) -1. Fork the repo -2. Fix an issue or create an issue and fix it -3. Create a Pull Request that fixes the issue. Follow [DEV-GUIDELINES](DEV-GUIDELINES.md) for a list of rules and best practices followed by project Helidon. -4. Watch your PR for pipeline results. If there are failures then fix them. -5. We will review your PR and merge as appropriate. +1. Ensure there is an issue created to track and discuss the fix or enhancement you intend to submit. +2. Fork this repository. +3. Create a branch in your fork to implement the changes. We recommend using the issue number as part of your branch name, e.g. 1234-fixes. Follow [DEV-GUIDELINES](DEV-GUIDELINES.md) for a list of rules and best practices followed by project Helidon. +4. Ensure that any documentation is updated with the changes that are required by your change. +5. Ensure that any samples are updated if the base image has been changed. +6. Submit the pull request. Do not leave the pull request blank. Explain exactly what your changes are meant to do and provide simple steps on how to validate. your changes. Ensure that you reference the issue you created as well. +7. We will assign the pull request to 2-3 people for review before it is merged ## Code of Conduct diff --git a/README.md b/README.md index f76cd1ee152..9c69fe62aaf 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ $ mvn validate -Pcopyright $ mvn verify -Pspotbugs ``` -**Documentatonn** +**Documentation** ```bash # At the root of the project @@ -128,7 +128,7 @@ but a couple are handy to use on your desktop to verify your changes. * Ask questions on Stack Overflow using the [helidon tag](https://stackoverflow.com/tags/helidon) * Join us on Slack: [#helidon-users](http://slack.helidon.io) -## Get Involved +## Contributing * Learn how to [contribute](CONTRIBUTING.md) * See [issues](https://github.com/oracle/helidon/issues) for issues you can help with diff --git a/all-config-meta.json b/all-config-meta.json deleted file mode 100644 index fe6f2421c93..00000000000 --- a/all-config-meta.json +++ /dev/null @@ -1,5804 +0,0 @@ -[ - { - "module": "io.helidon.webclient.http1", - "types": [ - { - "annotatedType": "io.helidon.webclient.http1.Http1ClientProtocolConfig", - "type": "io.helidon.webclient.http1.Http1ClientProtocolConfig", - "producers": [ - "io.helidon.webclient.http1.Http1ClientProtocolConfig#create(io.helidon.common.config.Config)", - "io.helidon.webclient.http1.Http1ClientProtocolConfig#builder()" - ], - "options": [ - { - "defaultValue": "true", - "description": "Sets whether the response header format is validated or not.\n

\n Defaults to `true`.\n

\n\n @return whether response header validation should be enabled", - "key": "validate-response-headers", - "method": "io.helidon.webclient.http1.Http1ClientProtocolConfig.Builder#validateResponseHeaders(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "256", - "description": "Configure the maximum allowed length of the status line from the response.\n\n @return maximum status line length", - "key": "max-status-line-length", - "method": "io.helidon.webclient.http1.Http1ClientProtocolConfig.Builder#maxStatusLineLength(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "false", - "description": "Sets whether the request header format is validated or not.\n

\n Defaults to `false` as user has control on the header creation.\n

\n\n @return whether request header validation should be enabled", - "key": "validate-request-headers", - "method": "io.helidon.webclient.http1.Http1ClientProtocolConfig.Builder#validateRequestHeaders(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "http_1_1", - "description": "", - "key": "name", - "method": "io.helidon.webclient.http1.Http1ClientProtocolConfig.Builder#name(java.lang.String)" - }, - { - "defaultValue": "16384", - "description": "Configure the maximum allowed header size of the response.\n\n @return maximum header size", - "key": "max-header-size", - "method": "io.helidon.webclient.http1.Http1ClientProtocolConfig.Builder#maxHeaderSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Whether to use keep alive by default.\n\n @return `true` for keeping connections alive and re-using them for multiple requests (default), `false`\n to create a new connection for each request", - "key": "default-keep-alive", - "method": "io.helidon.webclient.http1.Http1ClientProtocolConfig.Builder#defaultKeepAlive(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webclient.websocket", - "types": [ - { - "annotatedType": "io.helidon.webclient.websocket.WsClientProtocolConfig", - "type": "io.helidon.webclient.websocket.WsClientProtocolConfig", - "producers": [ - "io.helidon.webclient.websocket.WsClientProtocolConfig#create(io.helidon.common.config.Config)", - "io.helidon.webclient.websocket.WsClientProtocolConfig#builder()" - ], - "options": [ - { - "description": "", - "key": "sub-protocols", - "kind": "LIST", - "method": "io.helidon.webclient.websocket.WsClientProtocolConfig.Builder#subProtocols(java.util.List)" - }, - { - "defaultValue": "websocket", - "description": "", - "key": "name", - "method": "io.helidon.webclient.websocket.WsClientProtocolConfig.Builder#name(java.lang.String)" - } - ] - }, - { - "annotatedType": "io.helidon.webclient.websocket.WsClientConfig", - "type": "io.helidon.webclient.websocket.WsClient", - "producers": [ - "io.helidon.webclient.websocket.WsClientConfig#create(io.helidon.common.config.Config)", - "io.helidon.webclient.websocket.WsClientConfig#builder()", - "io.helidon.webclient.websocket.WsClient#create(io.helidon.webclient.websocket.WsClientConfig)" - ], - "options": [ - { - "defaultValue": "create()", - "description": "WebSocket specific configuration.\n\n @return protocol specific configuration", - "key": "protocol-config", - "method": "io.helidon.webclient.websocket.WsClientConfig.Builder#protocolConfig(io.helidon.webclient.websocket.WsClientProtocolConfig)", - "type": "io.helidon.webclient.websocket.WsClientProtocolConfig" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webclient.http2", - "types": [ - { - "annotatedType": "io.helidon.webclient.http2.Http2ClientProtocolConfig", - "type": "io.helidon.webclient.http2.Http2ClientProtocolConfig", - "producers": [ - "io.helidon.webclient.http2.Http2ClientProtocolConfig#create(io.helidon.common.config.Config)", - "io.helidon.webclient.http2.Http2ClientProtocolConfig#builder()" - ], - "options": [ - { - "defaultValue": "false", - "description": "Prior knowledge of HTTP/2 capabilities of the server. If server we are connecting to does not\n support HTTP/2 and prior knowledge is set to `false`, only features supported by HTTP/1 will be available\n and attempts to use HTTP/2 specific will throw an UnsupportedOperationException.\n

Plain text connection

\n If prior knowledge is set to `true`, we will not attempt an upgrade of connection and use prior knowledge.\n If prior knowledge is set to `false`, we will initiate an HTTP/1 connection and upgrade it to HTTP/2,\n if supported by the server.\n plaintext connection (`h2c`).\n

TLS protected connection

\n If prior knowledge is set to `true`, we will negotiate protocol using HTTP/2 only, failing if not supported.\n if prior knowledge is set to `false`, we will negotiate protocol using both HTTP/2 and HTTP/1, using the protocol\n supported by server.\n\n @return whether to use prior knowledge of HTTP/2", - "key": "prior-knowledge", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#priorKnowledge(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "PT0.1S", - "description": "Timeout for blocking between windows size check iterations.\n\n @return timeout", - "key": "flow-control-block-timeout", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#flowControlBlockTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "PT0.5S", - "description": "Timeout for ping probe used for checking healthiness of cached connections.\n Defaults to `PT0.5S`, which means 500 milliseconds.\n\n @return timeout", - "key": "ping-timeout", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#pingTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "16384", - "description": "Configure initial MAX_FRAME_SIZE setting for new HTTP/2 connections.\n Maximum size of data frames in bytes the client is prepared to accept from the server.\n Default value is 2^14(16_384).\n\n @return data frame size in bytes between 2^14(16_384) and 2^24-1(16_777_215)", - "key": "max-frame-size", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#maxFrameSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "false", - "description": "Check healthiness of cached connections with HTTP/2.0 ping frame.\n Defaults to `false`.\n\n @return use ping if true", - "key": "ping", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#ping(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "-1", - "description": "Configure initial MAX_HEADER_LIST_SIZE setting for new HTTP/2 connections.\n Sends to the server the maximum header field section size client is prepared to accept.\n Defaults to `-1`, which means \"unconfigured\".\n\n @return units of octets", - "key": "max-header-list-size", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#maxHeaderListSize(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "h2", - "description": "", - "key": "name", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#name(java.lang.String)" - }, - { - "defaultValue": "65535", - "description": "Configure INITIAL_WINDOW_SIZE setting for new HTTP/2 connections.\n Sends to the server the size of the largest frame payload client is willing to receive.\n Defaults to {@value io.helidon.http.http2.WindowSize#DEFAULT_WIN_SIZE}.\n\n @return units of octets", - "key": "initial-window-size", - "method": "io.helidon.webclient.http2.Http2ClientProtocolConfig.Builder#initialWindowSize(int)", - "type": "java.lang.Integer" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webclient.api", - "types": [ - { - "annotatedType": "io.helidon.webclient.api.WebClientConfig", - "prefix": "clients", - "type": "io.helidon.webclient.api.WebClient", - "standalone": true, - "inherits": [ - "io.helidon.webclient.api.HttpClientConfig" - ], - "producers": [ - "io.helidon.webclient.api.WebClientConfig#create(io.helidon.common.config.Config)", - "io.helidon.webclient.api.WebClientConfig#builder()", - "io.helidon.webclient.api.WebClient#create(io.helidon.webclient.api.WebClientConfig)" - ], - "options": [ - { - "description": "Configuration of client protocols.\n\n @return client protocol configurations", - "key": "protocol-configs", - "kind": "LIST", - "method": "io.helidon.webclient.api.WebClientConfig.Builder#protocolConfigs(java.util.List)", - "providerType": "io.helidon.webclient.spi.ProtocolConfigProvider", - "type": "io.helidon.webclient.spi.ProtocolConfig", - "provider": true - } - ] - }, - { - "annotatedType": "io.helidon.webclient.api.HttpClientConfig", - "type": "io.helidon.webclient.api.HttpClientConfig", - "inherits": [ - "io.helidon.webclient.api.HttpConfigBase" - ], - "producers": [ - "io.helidon.webclient.api.HttpClientConfig#create(io.helidon.common.config.Config)", - "io.helidon.webclient.api.HttpClientConfig#builder()" - ], - "options": [ - { - "defaultValue": "false", - "description": "Can be set to `true` to force the use of relative URIs in all requests,\n regardless of the presence or absence of proxies or no-proxy lists.\n\n @return relative URIs flag", - "key": "relative-uris", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#relativeUris(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Default headers to be used in every request from configuration.\n\n @return default headers", - "key": "default-headers", - "kind": "MAP", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#defaultHeadersMap(java.util.Map)" - }, - { - "description": "Configure the listener specific io.helidon.http.encoding.ContentEncodingContext.\n This method discards all previously registered ContentEncodingContext.\n If no content encoding context is registered, default encoding context is used.\n\n @return content encoding context", - "key": "content-encoding", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#contentEncoding(io.helidon.http.encoding.ContentEncodingContext)", - "type": "io.helidon.http.encoding.ContentEncodingContext" - }, - { - "defaultValue": "256", - "description": "Maximal size of the connection cache.\n For most HTTP protocols, we may cache connections to various endpoints for keep alive (or stream reuse in case of HTTP/2).\n This option limits the size. Setting this number lower than the \"usual\" number of target services will cause connections\n to be closed and reopened frequently.", - "key": "connection-cache-size", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#connectionCacheSize(int)", - "type": "java.lang.Integer" - }, - { - "description": "WebClient services.\n\n @return services to use with this web client", - "key": "services", - "kind": "LIST", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#services(java.util.List)", - "providerType": "io.helidon.webclient.spi.WebClientServiceProvider", - "type": "io.helidon.webclient.spi.WebClientService", - "provider": true - }, - { - "defaultValue": "create()", - "description": "Configure the listener specific io.helidon.http.media.MediaContext.\n This method discards all previously registered MediaContext.\n If no media context is registered, default media context is used.\n\n @return media context", - "key": "media-context", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#mediaContext(io.helidon.http.media.MediaContext)", - "type": "io.helidon.http.media.MediaContext" - }, - { - "description": "WebClient cookie manager.\n\n @return cookie manager to use", - "key": "cookie-manager", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#cookieManager(java.util.Optional)", - "type": "io.helidon.webclient.api.WebClientCookieManager" - }, - { - "defaultValue": "131072", - "description": "If the entity is expected to be smaller that this number of bytes, it would be buffered in memory to optimize performance.\n If bigger, streaming will be used.\n

\n Note that for some entity types we cannot use streaming, as they are already fully in memory (String, byte[]), for such\n cases, this option is ignored. Default is 128Kb.\n\n @return maximal number of bytes to buffer in memory for supported writers", - "key": "max-in-memory-entity", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#maxInMemoryEntity(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Whether Expect-100-Continue header is sent to verify server availability before sending an entity.\n

\n Defaults to `true`.\n

\n\n @return whether Expect:100-Continue header should be sent on streamed transfers", - "key": "send-expect-continue", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#sendExpectContinue(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Socket options for connections opened by this client.\n If there is a value explicitly configured on this type and on the socket options,\n the one configured on this type's builder will win:\n
    \n
  • #readTimeout()
  • \n
  • #connectTimeout()
  • \n
\n\n @return socket options", - "key": "socket-options", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#socketOptions(io.helidon.common.socket.SocketOptions)", - "type": "io.helidon.common.socket.SocketOptions" - }, - { - "defaultValue": "true", - "description": "Whether to share connection cache between all the WebClient instances in JVM.\n\n @return true if connection cache is shared", - "key": "share-connection-cache", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#shareConnectionCache(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "STRICT", - "description": "Configure media type parsing mode for HTTP `Content-Type` header.\n\n @return media type parsing mode", - "key": "media-type-parser-mode", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#mediaTypeParserMode(io.helidon.common.media.type.ParserMode)", - "type": "io.helidon.common.media.type.ParserMode", - "allowedValues": [ - { - "description": "", - "value": "STRICT" - }, - { - "description": "", - "value": "RELAXED" - } - ] - }, - { - "description": "Base uri used by the client in all requests.\n\n @return base uri of the client requests", - "key": "base-uri", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#baseUri(java.util.Optional)", - "type": "io.helidon.webclient.api.ClientUri" - }, - { - "defaultValue": "PT1S", - "description": "Socket 100-Continue read timeout. Default is 1 second.\n This read timeout is used when 100-Continue is sent by the client, before it sends an entity.\n\n @return read 100-Continue timeout duration", - "key": "read-continue-timeout", - "method": "io.helidon.webclient.api.HttpClientConfig.Builder#readContinueTimeout(java.time.Duration)", - "type": "java.time.Duration" - } - ] - }, - { - "annotatedType": "io.helidon.webclient.api.WebClientCookieManagerConfig", - "type": "io.helidon.webclient.api.WebClientCookieManager", - "producers": [ - "io.helidon.webclient.api.WebClientCookieManagerConfig#create(io.helidon.common.config.Config)", - "io.helidon.webclient.api.WebClientCookieManagerConfig#builder()", - "io.helidon.webclient.api.WebClientCookieManager#create(io.helidon.webclient.api.WebClientCookieManagerConfig)" - ], - "options": [ - { - "defaultValue": "false", - "description": "Whether automatic cookie store is enabled or not.\n\n @return status of cookie store", - "key": "automatic-store-enabled", - "method": "io.helidon.webclient.api.WebClientCookieManagerConfig.Builder#automaticStoreEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER", - "description": "Current cookie policy for this client.\n\n @return the cookie policy", - "key": "cookie-policy", - "method": "io.helidon.webclient.api.WebClientCookieManagerConfig.Builder#cookiePolicy(java.net.CookiePolicy)", - "type": "java.net.CookiePolicy" - }, - { - "description": "Map of default cookies to include in all requests if cookies enabled.\n\n @return map of default cookies", - "key": "default-cookies", - "kind": "MAP", - "method": "io.helidon.webclient.api.WebClientCookieManagerConfig.Builder#defaultCookies(java.util.Map)" - } - ] - }, - { - "annotatedType": "io.helidon.webclient.api.HttpConfigBase", - "type": "io.helidon.webclient.api.HttpConfigBase", - "producers": [ - "io.helidon.webclient.api.HttpConfigBase#create(io.helidon.common.config.Config)", - "io.helidon.webclient.api.HttpConfigBase#builder()" - ], - "options": [ - { - "description": "Read timeout.\n\n @return read timeout\n @see io.helidon.common.socket.SocketOptions#readTimeout()", - "key": "read-timeout", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#readTimeout(java.util.Optional)", - "type": "java.time.Duration" - }, - { - "defaultValue": "true", - "description": "Determines if connection keep alive is enabled (NOT socket keep alive, but HTTP connection keep alive, to re-use\n the same connection for multiple requests).\n\n @return keep alive for this connection\n @see io.helidon.common.socket.SocketOptions#socketKeepAlive()", - "key": "keep-alive", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#keepAlive(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Proxy configuration to be used for requests.\n\n @return proxy to use, defaults to Proxy#noProxy()", - "key": "proxy", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#proxy(io.helidon.webclient.api.Proxy)", - "type": "io.helidon.webclient.api.Proxy" - }, - { - "defaultValue": "true", - "description": "Whether to follow redirects.\n\n @return whether to follow redirects", - "key": "follow-redirects", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#followRedirects(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Connect timeout.\n\n @return connect timeout\n @see io.helidon.common.socket.SocketOptions#connectTimeout()", - "key": "connect-timeout", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#connectTimeout(java.util.Optional)", - "type": "java.time.Duration" - }, - { - "defaultValue": "10", - "description": "Max number of followed redirects.\n This is ignored if #followRedirects() option is `false`.\n\n @return max number of followed redirects", - "key": "max-redirects", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#maxRedirects(int)", - "type": "java.lang.Integer" - }, - { - "description": "TLS configuration for any TLS request from this client.\n TLS can also be configured per request.\n TLS is used when the protocol is set to `https`.\n\n @return TLS configuration to use", - "key": "tls", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#tls(io.helidon.common.tls.Tls)", - "type": "io.helidon.common.tls.Tls" - }, - { - "description": "Properties configured for this client. These properties are propagated through client request, to be used by\n services (and possibly for other purposes).\n\n @return map of client properties", - "key": "properties", - "kind": "MAP", - "method": "io.helidon.webclient.api.HttpConfigBase.Builder#properties(java.util.Map)" - } - ] - }, - { - "annotatedType": "io.helidon.webclient.api.Proxy.Builder", - "type": "io.helidon.webclient.api.Proxy", - "producers": [ - "io.helidon.webclient.api.Proxy.Builder#build()", - "io.helidon.webclient.api.Proxy#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "Sets a new password for the proxy.", - "key": "password", - "method": "io.helidon.webclient.api.Proxy.Builder#password(char[])" - }, - { - "description": "Sets a port value.", - "key": "port", - "method": "io.helidon.webclient.api.Proxy.Builder#port(int)", - "type": "java.lang.Integer" - }, - { - "description": "Sets a new host value.", - "key": "host", - "method": "io.helidon.webclient.api.Proxy.Builder#host(java.lang.String)" - }, - { - "description": "Sets a new username for the proxy.", - "key": "username", - "method": "io.helidon.webclient.api.Proxy.Builder#username(java.lang.String)" - }, - { - "defaultValue": "HTTP", - "description": "Sets a new proxy type.", - "key": "type", - "method": "io.helidon.webclient.api.Proxy.Builder#type(io.helidon.webclient.api.Proxy.ProxyType)", - "type": "io.helidon.webclient.api.Proxy.ProxyType", - "allowedValues": [ - { - "description": "No proxy.", - "value": "NONE" - }, - { - "description": "Proxy obtained from system.", - "value": "SYSTEM" - }, - { - "description": "HTTP proxy.", - "value": "HTTP" - } - ] - }, - { - "description": "Configure a host pattern that is not going through a proxy.\n

\n Options are:\n

    \n
  • IP Address, such as `192.168.1.1`
  • \n
  • IP V6 Address, such as `[2001:db8:85a3:8d3:1319:8a2e:370:7348]`
  • \n
  • Hostname, such as `localhost`
  • \n
  • Domain name, such as `helidon.io`
  • \n
  • Domain name and all sub-domains, such as `.helidon.io` (leading dot)
  • \n
  • Combination of all options from above with a port, such as `.helidon.io:80`
  • \n
", - "key": "no-proxy", - "kind": "LIST", - "method": "io.helidon.webclient.api.Proxy.Builder#addNoProxy(java.lang.String)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.metrics.api", - "types": [ - { - "annotatedType": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig", - "type": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig", - "producers": [ - "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig#builder()" - ], - "options": [ - { - "defaultValue": "PT10S", - "description": "Threshold in ms that characterizes whether a request is long running.\n\n @return threshold in ms indicating a long-running request", - "key": "long-running-requests.threshold", - "method": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig.Builder#longRunningRequestThreshold(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "false", - "description": "Whether KPI extended metrics are enabled.\n\n @return true if KPI extended metrics are enabled; false otherwise", - "key": "extended", - "method": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig.Builder#extended(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.ScopingConfig", - "type": "io.helidon.metrics.api.ScopingConfig", - "producers": [ - "io.helidon.metrics.api.ScopingConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.ScopingConfig#builder()" - ], - "options": [ - { - "defaultValue": "application", - "description": "Default scope value to associate with meters that are registered without an explicit setting; no setting means meters\n are assigned scope {@value io.helidon.metrics.api.Meter.Scope#DEFAULT}.\n\n @return default scope value", - "key": "default", - "method": "io.helidon.metrics.api.ScopingConfig.Builder#defaultValue(java.util.Optional)" - }, - { - "defaultValue": "scope", - "description": "Tag name for storing meter scope values in the underlying implementation meter registry.\n\n @return tag name for storing scope values", - "key": "tag-name", - "method": "io.helidon.metrics.api.ScopingConfig.Builder#tagName(java.util.Optional)" - }, - { - "description": "Settings for individual scopes.\n\n @return scope settings", - "key": "scopes", - "kind": "MAP", - "method": "io.helidon.metrics.api.ScopingConfig.Builder#scopes(java.util.Map)", - "type": "io.helidon.metrics.api.ScopeConfig" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.MetricsConfig", - "prefix": "metrics", - "type": "io.helidon.metrics.api.MetricsConfig", - "standalone": true, - "producers": [ - "io.helidon.metrics.api.MetricsConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.MetricsConfig#builder()" - ], - "options": [ - { - "description": "Whether automatic REST request metrics should be measured.\n\n @return true/false", - "key": "rest-request-enabled", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#restRequestEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Name for the application tag to be added to each meter ID.\n\n @return application tag name", - "key": "app-tag-name", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#appTagName(java.util.Optional)" - }, - { - "defaultValue": "observe", - "description": "Hints for role names the user is expected to be in.\n\n @return list of hints", - "key": "roles", - "kind": "LIST", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#roles(java.util.List)" - }, - { - "description": "Key performance indicator metrics settings.\n\n @return key performance indicator metrics settings", - "key": "key-performance-indicators", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#keyPerformanceIndicatorMetricsConfig(io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig)", - "type": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig" - }, - { - "defaultValue": "true", - "description": "Whether to allow anybody to access the endpoint.\n\n @return whether to permit access to metrics endpoint to anybody, defaults to `true`\n @see #roles()", - "key": "permit-all", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#permitAll(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Settings related to scoping management.\n\n @return scoping settings", - "key": "scoping", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#scoping(io.helidon.metrics.api.ScopingConfig)", - "type": "io.helidon.metrics.api.ScopingConfig" - }, - { - "description": "Global tags.\n\n @return name/value pairs for global tags", - "key": "tags", - "kind": "LIST", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#tags(java.util.List)", - "type": "io.helidon.metrics.api.Tag" - }, - { - "description": "Value for the application tag to be added to each meter ID.\n\n @return application tag value", - "key": "app-name", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#appName(java.util.Optional)" - }, - { - "defaultValue": "true", - "description": "Whether metrics functionality is enabled.\n\n @return if metrics are configured to be enabled", - "key": "enabled", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.ScopeConfig", - "type": "io.helidon.metrics.api.ScopeConfig", - "producers": [ - "io.helidon.metrics.api.ScopeConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.ScopeConfig#builder()" - ], - "options": [ - { - "description": "Regular expression for meter names to include.\n\n @return include expression", - "key": "filter.include", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#include(java.util.Optional)", - "type": "java.util.regex.Pattern" - }, - { - "description": "Name of the scope to which the configuration applies.\n\n @return scope name", - "key": "name", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#name(java.lang.String)" - }, - { - "description": "Regular expression for meter names to exclude.\n\n @return exclude expression", - "key": "filter.exclude", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#exclude(java.util.Optional)", - "type": "java.util.regex.Pattern" - }, - { - "defaultValue": "true", - "description": "Whether the scope is enabled.\n\n @return if the scope is enabled", - "key": "enabled", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.ComponentMetricsSettings.Builder", - "prefix": "metrics", - "type": "io.helidon.metrics.api.ComponentMetricsSettings.Builder", - "options": [ - { - "description": "Sets whether metrics should be enabled for the component.", - "key": "enabled", - "method": "io.helidon.metrics.api.ComponentMetricsSettings.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.Tag", - "type": "io.helidon.metrics.api.Tag", - "options": [] - } - ] - } -] -[ - { - "module": "io.helidon.metrics.api", - "types": [ - { - "annotatedType": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig", - "type": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig", - "producers": [ - "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig#builder()" - ], - "options": [ - { - "defaultValue": "PT10S", - "description": "Threshold in ms that characterizes whether a request is long running.\n\n @return threshold in ms indicating a long-running request", - "key": "long-running-requests.threshold", - "method": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig.Builder#longRunningRequestThreshold(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "false", - "description": "Whether KPI extended metrics are enabled.\n\n @return true if KPI extended metrics are enabled; false otherwise", - "key": "extended", - "method": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig.Builder#extended(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.ScopingConfig", - "type": "io.helidon.metrics.api.ScopingConfig", - "producers": [ - "io.helidon.metrics.api.ScopingConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.ScopingConfig#builder()" - ], - "options": [ - { - "defaultValue": "application", - "description": "Default scope value to associate with meters that are registered without an explicit setting; no setting means meters\n are assigned scope {@value io.helidon.metrics.api.Meter.Scope#DEFAULT}.\n\n @return default scope value", - "key": "default", - "method": "io.helidon.metrics.api.ScopingConfig.Builder#defaultValue(java.util.Optional)" - }, - { - "defaultValue": "scope", - "description": "Tag name for storing meter scope values in the underlying implementation meter registry.\n\n @return tag name for storing scope values", - "key": "tag-name", - "method": "io.helidon.metrics.api.ScopingConfig.Builder#tagName(java.util.Optional)" - }, - { - "description": "Settings for individual scopes.\n\n @return scope settings", - "key": "scopes", - "kind": "MAP", - "method": "io.helidon.metrics.api.ScopingConfig.Builder#scopes(java.util.Map)", - "type": "io.helidon.metrics.api.ScopeConfig" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.MetricsConfig", - "prefix": "metrics", - "type": "io.helidon.metrics.api.MetricsConfig", - "standalone": true, - "producers": [ - "io.helidon.metrics.api.MetricsConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.MetricsConfig#builder()" - ], - "options": [ - { - "description": "Whether automatic REST request metrics should be measured.\n\n @return true/false", - "key": "rest-request-enabled", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#restRequestEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Name for the application tag to be added to each meter ID.\n\n @return application tag name", - "key": "app-tag-name", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#appTagName(java.util.Optional)" - }, - { - "defaultValue": "observe", - "description": "Hints for role names the user is expected to be in.\n\n @return list of hints", - "key": "roles", - "kind": "LIST", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#roles(java.util.List)" - }, - { - "description": "Key performance indicator metrics settings.\n\n @return key performance indicator metrics settings", - "key": "key-performance-indicators", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#keyPerformanceIndicatorMetricsConfig(io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig)", - "type": "io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig" - }, - { - "defaultValue": "true", - "description": "Whether to allow anybody to access the endpoint.\n\n @return whether to permit access to metrics endpoint to anybody, defaults to `true`\n @see #roles()", - "key": "permit-all", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#permitAll(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Settings related to scoping management.\n\n @return scoping settings", - "key": "scoping", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#scoping(io.helidon.metrics.api.ScopingConfig)", - "type": "io.helidon.metrics.api.ScopingConfig" - }, - { - "description": "Global tags.\n\n @return name/value pairs for global tags", - "key": "tags", - "kind": "LIST", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#tags(java.util.List)", - "type": "io.helidon.metrics.api.Tag" - }, - { - "description": "Value for the application tag to be added to each meter ID.\n\n @return application tag value", - "key": "app-name", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#appName(java.util.Optional)" - }, - { - "defaultValue": "true", - "description": "Whether metrics functionality is enabled.\n\n @return if metrics are configured to be enabled", - "key": "enabled", - "method": "io.helidon.metrics.api.MetricsConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.ScopeConfig", - "type": "io.helidon.metrics.api.ScopeConfig", - "producers": [ - "io.helidon.metrics.api.ScopeConfig#create(io.helidon.common.config.Config)", - "io.helidon.metrics.api.ScopeConfig#builder()" - ], - "options": [ - { - "description": "Regular expression for meter names to include.\n\n @return include expression", - "key": "filter.include", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#include(java.util.Optional)", - "type": "java.util.regex.Pattern" - }, - { - "description": "Name of the scope to which the configuration applies.\n\n @return scope name", - "key": "name", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#name(java.lang.String)" - }, - { - "description": "Regular expression for meter names to exclude.\n\n @return exclude expression", - "key": "filter.exclude", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#exclude(java.util.Optional)", - "type": "java.util.regex.Pattern" - }, - { - "defaultValue": "true", - "description": "Whether the scope is enabled.\n\n @return if the scope is enabled", - "key": "enabled", - "method": "io.helidon.metrics.api.ScopeConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.ComponentMetricsSettings.Builder", - "prefix": "metrics", - "type": "io.helidon.metrics.api.ComponentMetricsSettings.Builder", - "options": [ - { - "description": "Sets whether metrics should be enabled for the component.", - "key": "enabled", - "method": "io.helidon.metrics.api.ComponentMetricsSettings.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.metrics.api.Tag", - "type": "io.helidon.metrics.api.Tag", - "options": [] - } - ] - } -] -[ - { - "module": "io.helidon.tracing", - "types": [ - { - "annotatedType": "io.helidon.tracing.TracerBuilder", - "description": "Tracer configuration.", - "type": "io.helidon.tracing.TracerBuilder", - "producers": [ - "io.helidon.tracing.TracerBuilder#create(io.helidon.common.config.Config)" - ], - "options": [] - } - ] - } -] -[ - { - "module": "io.helidon.tracing.providers.opentracing", - "types": [ - { - "annotatedType": "io.helidon.tracing.providers.opentracing.OpenTracingTracerBuilder", - "description": "OpenTracing tracer configuration.", - "type": "io.helidon.tracing.providers.opentracing.OpenTracingTracerBuilder", - "producers": [ - "io.helidon.tracing.providers.opentracing.OpenTracingTracerBuilder#create(io.helidon.common.config.Config)" - ], - "options": [] - } - ] - } -] -[ - { - "module": "io.helidon.tracing", - "types": [ - { - "annotatedType": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder", - "description": "Jaeger tracer configuration.", - "prefix": "tracing", - "type": "io.helidon.tracing.Tracer", - "standalone": true, - "inherits": [ - "io.helidon.tracing.TracerBuilder" - ], - "producers": [ - "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#build()" - ], - "options": [ - { - "defaultValue": "512", - "description": "Maximum Export Batch Size of exporter requests.", - "key": "max-export-batch-size", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#maxExportBatchSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "PT5S", - "description": "Schedule Delay of exporter requests.", - "key": "schedule-delay", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#scheduleDelay(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "JAEGER", - "description": "Add propagation format to use.", - "key": "propagation", - "kind": "LIST", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#addPropagation(io.helidon.tracing.providers.jaeger.JaegerTracerBuilder.PropagationFormat)", - "type": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder.PropagationFormat", - "allowedValues": [ - { - "description": "The Zipkin B3 trace context propagation format using multiple headers.", - "value": "B3" - }, - { - "description": "B3 trace context propagation using a single header.", - "value": "B3_SINGLE" - }, - { - "description": "The Jaeger trace context propagation format.", - "value": "JAEGER" - }, - { - "description": "The W3C trace context propagation format.", - "value": "W3C" - } - ] - }, - { - "description": "Private key in PEM format.", - "key": "private-key-pem", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#privateKey(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "defaultValue": "PT10S", - "description": "Timeout of exporter requests.", - "key": "exporter-timeout", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#exporterTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "CONSTANT", - "description": "Sampler type.\n

\n See Sampler types.", - "key": "sampler-type", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#samplerType(io.helidon.tracing.providers.jaeger.JaegerTracerBuilder.SamplerType)", - "type": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder.SamplerType", - "allowedValues": [ - { - "description": "Constant sampler always makes the same decision for all traces.\n It either samples all traces `1` or none of them `0`.", - "value": "CONSTANT" - }, - { - "description": "Ratio of the requests to sample, double value.", - "value": "RATIO" - } - ] - }, - { - "description": "Trusted certificates in PEM format.", - "key": "trusted-cert-pem", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#trustedCertificates(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "defaultValue": "batch", - "description": "Span Processor type used.", - "key": "span-processor-type", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#spanProcessorType(io.helidon.tracing.providers.jaeger.JaegerTracerBuilder.SpanProcessorType)", - "type": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder.SpanProcessorType", - "allowedValues": [ - { - "description": "Simple Span Processor.", - "value": "SIMPLE" - }, - { - "description": "Batch Span Processor.", - "value": "BATCH" - } - ] - }, - { - "defaultValue": "1", - "description": "The sampler parameter (number).", - "key": "sampler-param", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#samplerParam(java.lang.Number)", - "type": "java.lang.Number" - }, - { - "description": "Certificate of client in PEM format.", - "key": "client-cert-pem", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#clientCertificate(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "defaultValue": "2048", - "description": "Maximum Queue Size of exporter requests.", - "key": "max-queue-size", - "method": "io.helidon.tracing.providers.jaeger.JaegerTracerBuilder#maxQueueSize(int)", - "type": "java.lang.Integer" - } - ] - } - ] - } -] -[ - { - "module": "io.opentracing.api", - "types": [ - { - "annotatedType": "io.helidon.tracing.providers.zipkin.ZipkinTracerBuilder", - "description": "Zipkin tracer configuration", - "prefix": "tracing", - "type": "io.opentracing.Tracer", - "standalone": true, - "inherits": [ - "io.helidon.tracing.providers.opentracing.OpenTracingTracerBuilder" - ], - "producers": [ - "io.helidon.tracing.providers.zipkin.ZipkinTracerBuilder#build()" - ], - "options": [ - { - "defaultValue": "V2", - "description": "Version of Zipkin API to use.\n Defaults to Version#V2.", - "key": "api-version", - "method": "io.helidon.tracing.providers.zipkin.ZipkinTracerBuilder#version(io.helidon.tracing.providers.zipkin.ZipkinTracerBuilder.Version)", - "type": "io.helidon.tracing.providers.zipkin.ZipkinTracerBuilder.Version", - "allowedValues": [ - { - "description": "Version 1.", - "value": "V1" - }, - { - "description": "Version 2.", - "value": "V2" - } - ] - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.dbclient.jdbc", - "types": [ - { - "annotatedType": "io.helidon.dbclient.jdbc.JdbcParametersConfig", - "prefix": "parameters", - "type": "io.helidon.dbclient.jdbc.JdbcParametersConfig", - "producers": [ - "io.helidon.dbclient.jdbc.JdbcParametersConfig#create(io.helidon.common.config.Config)", - "io.helidon.dbclient.jdbc.JdbcParametersConfig#builder()" - ], - "options": [ - { - "defaultValue": "true", - "description": "Use java.sql.PreparedStatement#setBinaryStream(int, java.io.InputStream, int) binding\n for `byte[]` values.\n Default value is `true`.\n\n @return whether to use java.io.ByteArrayInputStream binding", - "key": "use-byte-array-binding", - "method": "io.helidon.dbclient.jdbc.JdbcParametersConfig.Builder#useByteArrayBinding(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Use java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, int) binding\n for String values with length above #stringBindingSize() limit.\n Default value is `true`.\n\n @return whether to use java.io.CharArrayReader binding", - "key": "use-string-binding", - "method": "io.helidon.dbclient.jdbc.JdbcParametersConfig.Builder#useStringBinding(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Use java.sql.PreparedStatement#setTimestamp(int, java.sql.Timestamp)\n to set java.time.LocalTime values when `true`\n or use java.sql.PreparedStatement#setTime(int, java.sql.Time) when `false`.\n Default value is `true`.\n

This option is vendor specific. Most of the databases are fine with java.sql.Timestamp,\n but for example SQL Server requires java.sql.Time.\n This option does not apply when #setObjectForJavaTime() is set to `true`.\n\n @return whether to use java.sql.Timestamp instead of java.sql.Time\n for java.time.LocalTime values", - "key": "timestamp-for-local-time", - "method": "io.helidon.dbclient.jdbc.JdbcParametersConfig.Builder#timestampForLocalTime(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Use SQL `NCHAR`, `NVARCHAR` or `LONGNVARCHAR` value conversion\n for String values.\n Default value is `false`.\n\n @return whether NString conversion is used", - "key": "use-n-string", - "method": "io.helidon.dbclient.jdbc.JdbcParametersConfig.Builder#useNString(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "1024", - "description": "String values with length above this limit will be bound\n using java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, int)\n if #useStringBinding() is set to `true`.\n Default value is `1024`.\n\n @return String values length limit for java.io.CharArrayReader binding", - "key": "string-binding-size", - "method": "io.helidon.dbclient.jdbc.JdbcParametersConfig.Builder#stringBindingSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Set all `java.time` Date/Time values directly using java.sql.PreparedStatement#setObject(int, Object).\n This option shall work fine for recent JDBC drivers.\n Default value is `true`.\n\n @return whether to use java.sql.PreparedStatement#setObject(int, Object) for `java.time` Date/Time values", - "key": "set-object-for-java-time", - "method": "io.helidon.dbclient.jdbc.JdbcParametersConfig.Builder#setObjectForJavaTime(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.scheduling", - "types": [ - { - "annotatedType": "io.helidon.scheduling.TaskConfig", - "type": "io.helidon.scheduling.TaskConfig", - "producers": [ - "io.helidon.scheduling.TaskConfig#create(io.helidon.common.config.Config)", - "io.helidon.scheduling.TaskConfig#builder()" - ], - "options": [] - }, - { - "annotatedType": "io.helidon.scheduling.FixedRateConfig", - "type": "io.helidon.scheduling.FixedRate", - "inherits": [ - "io.helidon.scheduling.TaskConfig" - ], - "producers": [ - "io.helidon.scheduling.FixedRateConfig#create(io.helidon.common.config.Config)", - "io.helidon.scheduling.FixedRateConfig#builder()", - "io.helidon.scheduling.FixedRate#create(io.helidon.scheduling.FixedRateConfig)" - ], - "options": [ - { - "description": "Fixed rate delay between each invocation. Time unit is by default java.util.concurrent.TimeUnit#SECONDS,\n can be specified with io.helidon.scheduling.FixedRateConfig.Builder#timeUnit(java.util.concurrent.TimeUnit).\n\n @return delay between each invocation", - "key": "delay", - "method": "io.helidon.scheduling.FixedRateConfig.Builder#delay(long)", - "type": "java.lang.Long", - "required": true - }, - { - "defaultValue": "SINCE_PREVIOUS_START", - "description": "Configure whether the delay between the invocations should be calculated from the time when previous task started or ended.\n Delay type is by default FixedRate.DelayType#SINCE_PREVIOUS_START.\n\n @return delay type", - "key": "delay-type", - "method": "io.helidon.scheduling.FixedRateConfig.Builder#delayType(io.helidon.scheduling.FixedRate.DelayType)", - "type": "io.helidon.scheduling.FixedRate.DelayType", - "allowedValues": [ - { - "description": "Next invocation delay is measured from the previous invocation task start.", - "value": "SINCE_PREVIOUS_START" - }, - { - "description": "Next invocation delay is measured from the previous invocation task end.", - "value": "SINCE_PREVIOUS_END" - } - ] - }, - { - "defaultValue": "TimeUnit.SECONDS", - "description": "java.util.concurrent.TimeUnit TimeUnit used for interpretation of values provided with\n io.helidon.scheduling.FixedRateConfig.Builder#delay(long)\n and io.helidon.scheduling.FixedRateConfig.Builder#initialDelay(long).\n\n @return time unit for interpreting values\n in io.helidon.scheduling.FixedRateConfig.Builder#delay(long)\n and io.helidon.scheduling.FixedRateConfig.Builder#initialDelay(long)", - "key": "time-unit", - "method": "io.helidon.scheduling.FixedRateConfig.Builder#timeUnit(java.util.concurrent.TimeUnit)", - "type": "java.util.concurrent.TimeUnit", - "allowedValues": [ - { - "description": "", - "value": "NANOSECONDS" - }, - { - "description": "", - "value": "MICROSECONDS" - }, - { - "description": "", - "value": "MILLISECONDS" - }, - { - "description": "", - "value": "SECONDS" - }, - { - "description": "", - "value": "MINUTES" - }, - { - "description": "", - "value": "HOURS" - }, - { - "description": "", - "value": "DAYS" - } - ] - }, - { - "defaultValue": "0", - "description": "Initial delay of the first invocation. Time unit is by default java.util.concurrent.TimeUnit#SECONDS,\n can be specified with\n io.helidon.scheduling.FixedRateConfig.Builder#timeUnit(java.util.concurrent.TimeUnit) timeUnit().\n\n @return initial delay value", - "key": "initial-delay", - "method": "io.helidon.scheduling.FixedRateConfig.Builder#initialDelay(long)", - "type": "java.lang.Long" - } - ] - }, - { - "annotatedType": "io.helidon.scheduling.CronConfig", - "type": "io.helidon.scheduling.Cron", - "inherits": [ - "io.helidon.scheduling.TaskConfig" - ], - "producers": [ - "io.helidon.scheduling.CronConfig#create(io.helidon.common.config.Config)", - "io.helidon.scheduling.CronConfig#builder()", - "io.helidon.scheduling.Cron#create(io.helidon.scheduling.CronConfig)" - ], - "options": [ - { - "defaultValue": "true", - "description": "Allow concurrent execution if previous task didn't finish before next execution.\n Default value is `true`.\n\n @return true for allow concurrent execution.", - "key": "concurrent", - "method": "io.helidon.scheduling.CronConfig.Builder#concurrentExecution(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Cron expression for specifying period of execution.\n

\n Examples:\n

    \n
  • `0/2 * * * * ? *` - Every 2 seconds
  • \n
  • `0 45 9 ? * *` - Every day at 9:45
  • \n
  • `0 15 8 ? * MON-FRI` - Every workday at 8:15
  • \n
\n\n @return cron expression", - "key": "expression", - "method": "io.helidon.scheduling.CronConfig.Builder#expression(java.lang.String)", - "required": true - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.config.tests.config.metadata.meta.api", - "types": [ - { - "annotatedType": "io.helidon.config.tests.config.metadata.meta.api.MyBuilder", - "description": "builder", - "type": "io.helidon.config.tests.config.metadata.meta.api.MyTarget", - "inherits": [ - "io.helidon.config.tests.config.metadata.meta.api.AbstractBuilder" - ], - "producers": [ - "io.helidon.config.tests.config.metadata.meta.api.MyBuilder#build()" - ], - "options": [ - { - "defaultValue": "message", - "description": "message description", - "key": "message", - "method": "io.helidon.config.tests.config.metadata.meta.api.MyBuilder#message(java.lang.String)" - }, - { - "defaultValue": "42", - "description": "type description", - "key": "type", - "method": "io.helidon.config.tests.config.metadata.meta.api.MyBuilder#type(int)", - "type": "java.lang.Integer", - "allowedValues": [ - { - "description": "answer", - "value": "42" - }, - { - "description": "no answer", - "value": "0" - } - ] - } - ] - }, - { - "annotatedType": "io.helidon.config.tests.config.metadata.meta.api.AbstractBuilder", - "description": "abstract builder", - "type": "io.helidon.config.tests.config.metadata.meta.api.AbstractBuilder", - "options": [ - { - "description": "abstract description", - "key": "abstract-message", - "method": "io.helidon.config.tests.config.metadata.meta.api.AbstractBuilder#abstractMessage(java.lang.String)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.config.tests.config.metadata.builder.api", - "types": [ - { - "annotatedType": "io.helidon.config.tests.config.metadata.builder.api.MyAbstract", - "description": "abstract builder", - "type": "io.helidon.config.tests.config.metadata.builder.api.MyAbstract", - "producers": [ - "io.helidon.config.tests.config.metadata.builder.api.MyAbstract#create(io.helidon.common.config.Config)", - "io.helidon.config.tests.config.metadata.builder.api.MyAbstract#builder()" - ], - "options": [ - { - "description": "abstract description", - "key": "abstract-message", - "method": "io.helidon.config.tests.config.metadata.builder.api.MyAbstract.Builder#abstractMessage(java.lang.String)" - } - ] - }, - { - "annotatedType": "io.helidon.config.tests.config.metadata.builder.api.MyTarget", - "description": "builder", - "type": "io.helidon.config.tests.config.metadata.builder.api.MyTarget", - "inherits": [ - "io.helidon.config.tests.config.metadata.builder.api.MyAbstract" - ], - "producers": [ - "io.helidon.config.tests.config.metadata.builder.api.MyTarget#create(io.helidon.common.config.Config)", - "io.helidon.config.tests.config.metadata.builder.api.MyTarget#builder()" - ], - "options": [ - { - "defaultValue": "message", - "description": "message description", - "key": "message", - "method": "io.helidon.config.tests.config.metadata.builder.api.MyTarget.Builder#message(java.lang.String)" - }, - { - "defaultValue": "42", - "description": "type description", - "key": "type", - "method": "io.helidon.config.tests.config.metadata.builder.api.MyTarget.Builder#type(int)", - "type": "java.lang.Integer", - "allowedValues": [ - { - "description": "answer", - "value": "42" - }, - { - "description": "no answer", - "value": "0" - } - ] - } - ] - } - ] - } -] -[ - { - "module": "microprofile.config.api", - "types": [ - { - "annotatedType": "io.helidon.config.mp.MpConfigBuilder", - "prefix": "mp.config", - "type": "org.eclipse.microprofile.config.Config", - "standalone": true, - "producers": [ - "io.helidon.config.mp.MpConfigBuilder#build()" - ], - "options": [ - { - "description": "Configure an explicit profile name.", - "key": "profile", - "method": "io.helidon.config.mp.MpConfigBuilder#profile(java.lang.String)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.util", - "types": [ - { - "annotatedType": "io.helidon.security.util.TokenHandler.Builder", - "type": "io.helidon.security.util.TokenHandler", - "producers": [ - "io.helidon.security.util.TokenHandler.Builder#build()", - "io.helidon.security.util.TokenHandler#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "Set the token pattern (Regular expression) to extract the token.", - "key": "regexp", - "method": "io.helidon.security.util.TokenHandler.Builder#tokenPattern(java.util.regex.Pattern)" - }, - { - "description": "Set the prefix of header value to extract the token.", - "key": "prefix", - "method": "io.helidon.security.util.TokenHandler.Builder#tokenPrefix(java.lang.String)" - }, - { - "description": "Set the name of header to look into to extract the token.", - "key": "header", - "method": "io.helidon.security.util.TokenHandler.Builder#tokenHeader(java.lang.String)" - }, - { - "description": "Token format for creating outbound tokens.", - "key": "format", - "method": "io.helidon.security.util.TokenHandler.Builder#tokenFormat(java.lang.String)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security", - "types": [ - { - "annotatedType": "io.helidon.security.Security.Builder", - "description": "Configuration of security providers, integration and other security options", - "prefix": "security", - "type": "io.helidon.security.Security", - "standalone": true, - "producers": [ - "io.helidon.security.Security.Builder#build()", - "io.helidon.security.Security#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "defaultValue": "FIRST", - "description": "Type of the policy.", - "key": "provider-policy.type", - "method": "io.helidon.security.Security.Builder#providerSelectionPolicy(java.util.function.Function)", - "type": "io.helidon.security.ProviderSelectionPolicyType", - "allowedValues": [ - { - "description": "Choose first provider from the list by default.\n Choose provider with the name defined when explicit provider requested.", - "value": "FIRST" - }, - { - "description": "Can compose multiple providers together to form a single\n logical provider.", - "value": "COMPOSITE" - }, - { - "description": "Explicit class for a custom ProviderSelectionPolicyType.", - "value": "CLASS" - } - ] - }, - { - "description": "ID of the default authentication provider", - "key": "default-authentication-provider", - "method": "io.helidon.security.Security.Builder#authenticationProvider(io.helidon.security.spi.AuthenticationProvider)", - "providerType": "java.lang.String", - "provider": true - }, - { - "description": "Name of the secret provider", - "key": "secrets.*.provider", - "method": "io.helidon.security.Security.Builder#addSecret(java.lang.String, io.helidon.security.spi.SecretsProvider, T)" - }, - { - "description": "Provider selection policy class name, only used when type is set to CLASS", - "key": "provider-policy.class-name", - "method": "io.helidon.security.Security.Builder#providerSelectionPolicy(java.util.function.Function)", - "type": "java.lang.Class" - }, - { - "description": "Configuration specific to the secret provider", - "key": "secrets.*.config", - "method": "io.helidon.security.Security.Builder#addSecret(java.lang.String, io.helidon.security.spi.SecretsProvider, T)", - "providerType": "io.helidon.security.SecretsProviderConfig", - "type": "io.helidon.security.SecretsProviderConfig", - "provider": true - }, - { - "defaultValue": "true", - "description": "Whether or not tracing should be enabled. If set to false, security tracer will be a no-op tracer.", - "key": "tracing.enabled", - "method": "io.helidon.security.Security.Builder#tracingEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Server time to use when evaluating security policies that depend on time.", - "key": "environment.server-time", - "method": "io.helidon.security.Security.Builder#serverTime(io.helidon.security.SecurityTime)", - "type": "io.helidon.security.SecurityTime" - }, - { - "description": "ID of the default authorization provider", - "key": "default-authorization-provider", - "method": "io.helidon.security.Security.Builder#authorizationProvider(io.helidon.security.spi.AuthorizationProvider)" - }, - { - "description": "Configured secrets", - "key": "secrets", - "kind": "LIST", - "method": "io.helidon.security.Security.Builder#addSecret(java.lang.String, io.helidon.security.spi.SecretsProvider, T)", - "type": "io.helidon.common.config.Config" - }, - { - "description": "Add a provider, works as #addProvider(io.helidon.security.spi.SecurityProvider, String), where the name is set\n to {@link\n Class#getSimpleName()}.", - "key": "providers", - "kind": "LIST", - "method": "io.helidon.security.Security.Builder#addProvider(io.helidon.security.spi.SecurityProvider)", - "providerType": "io.helidon.security.spi.SecurityProvider", - "type": "io.helidon.security.spi.SecurityProvider", - "provider": true, - "required": true - }, - { - "defaultValue": "true", - "description": "Security can be disabled using configuration, or explicitly.\n By default, security instance is enabled.\n Disabled security instance will not perform any checks and allow\n all requests.", - "key": "enabled", - "method": "io.helidon.security.Security.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Name of the secret, used for lookup", - "key": "secrets.*.name", - "method": "io.helidon.security.Security.Builder#addSecret(java.lang.String, io.helidon.security.spi.SecretsProvider, T)" - } - ] - }, - { - "annotatedType": "io.helidon.security.SecurityTime.Builder", - "type": "io.helidon.security.SecurityTime", - "producers": [ - "io.helidon.security.SecurityTime.Builder#build()", - "io.helidon.security.SecurityTime#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "defaultValue": "0", - "description": "Configure a time-shift in seconds, to move the current time to past or future.", - "key": "shift-by-seconds", - "method": "io.helidon.security.SecurityTime.Builder#shiftBySeconds(long)", - "type": "java.lang.Long" - }, - { - "description": "Set an explicit value for one of the time fields (such as ChronoField#YEAR).", - "key": "year", - "method": "io.helidon.security.SecurityTime.Builder#value(java.time.temporal.ChronoField, long)", - "type": "java.lang.Long" - }, - { - "description": "Set an explicit value for one of the time fields (such as ChronoField#YEAR).", - "key": "millisecond", - "method": "io.helidon.security.SecurityTime.Builder#value(java.time.temporal.ChronoField, long)", - "type": "java.lang.Long" - }, - { - "description": "Set an explicit value for one of the time fields (such as ChronoField#YEAR).", - "key": "minute", - "method": "io.helidon.security.SecurityTime.Builder#value(java.time.temporal.ChronoField, long)", - "type": "java.lang.Long" - }, - { - "description": "Set an explicit value for one of the time fields (such as ChronoField#YEAR).", - "key": "second", - "method": "io.helidon.security.SecurityTime.Builder#value(java.time.temporal.ChronoField, long)", - "type": "java.lang.Long" - }, - { - "description": "Override current time zone. The time will represent the SAME instant, in an explicit timezone.\n

\n If we are in a UTC time zone and you set the timezone to \"Europe/Prague\", the time will be shifted by the offset\n of Prague (e.g. if it is noon right now in UTC, you would get 14:00).", - "key": "time-zone", - "method": "io.helidon.security.SecurityTime.Builder#timeZone(java.time.ZoneId)", - "type": "java.time.ZoneId" - }, - { - "description": "Set an explicit value for one of the time fields (such as ChronoField#YEAR).", - "key": "month", - "method": "io.helidon.security.SecurityTime.Builder#value(java.time.temporal.ChronoField, long)", - "type": "java.lang.Long" - }, - { - "description": "Set an explicit value for one of the time fields (such as ChronoField#YEAR).", - "key": "day-of-month", - "method": "io.helidon.security.SecurityTime.Builder#value(java.time.temporal.ChronoField, long)", - "type": "java.lang.Long" - }, - { - "description": "Set an explicit value for one of the time fields (such as ChronoField#YEAR).", - "key": "hour-of-day", - "method": "io.helidon.security.SecurityTime.Builder#value(java.time.temporal.ChronoField, long)", - "type": "java.lang.Long" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.jwt", - "types": [ - { - "annotatedType": "io.helidon.security.providers.jwt.JwtProvider.Builder", - "description": "JWT authentication provider", - "prefix": "jwt", - "type": "io.helidon.security.providers.jwt.JwtProvider", - "producers": [ - "io.helidon.security.providers.jwt.JwtProvider.Builder#build()", - "io.helidon.security.providers.jwt.JwtProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.AuthenticationProvider" - ], - "options": [ - { - "defaultValue": "true", - "description": "Whether to authenticate requests.", - "key": "authenticate", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#authenticate(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "JWK resource used to verify JWTs created by other parties.", - "key": "atn-token.jwk.resource", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#verifyJwk(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "defaultValue": "true", - "description": "Claim `groups` from JWT will be used to automatically add\n groups to current subject (may be used with jakarta.annotation.security.RolesAllowed annotation).", - "key": "use-jwt-groups", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#useJwtGroups(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Whether to allow impersonation by explicitly overriding\n username from outbound requests using io.helidon.security.EndpointConfig#PROPERTY_OUTBOUND_ID\n property.\n By default this is not allowed and identity can only be propagated.", - "key": "allow-impersonation", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#allowImpersonation(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Issuer used to create new JWTs.", - "key": "sign-token.jwt-issuer", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#issuer(java.lang.String)" - }, - { - "defaultValue": "USER", - "description": "Principal type this provider extracts (and also propagates).", - "key": "principal-type", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#subjectType(io.helidon.security.SubjectType)", - "type": "io.helidon.security.SubjectType", - "allowedValues": [ - { - "description": "", - "value": "USER" - }, - { - "description": "", - "value": "SERVICE" - } - ] - }, - { - "description": "JWK resource used to sign JWTs created by us.", - "key": "sign-token.jwk.resource", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#signJwk(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "defaultValue": "false", - "description": "Whether authentication is required.\n By default, request will fail if the username cannot be extracted.\n If set to false, request will process and this provider will abstain.", - "key": "optional", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#optional(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Whether to propagate identity.", - "key": "propagate", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#propagate(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Configure support for unsigned JWT.\n If this is set to `true` any JWT that has algorithm\n set to `none` and no `kid` defined will be accepted.\n Note that this has serious security impact - if JWT can be sent\n from a third party, this allows the third party to send ANY JWT\n and it would be accpted as valid.", - "key": "allow-unsigned", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#allowUnsigned(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Token handler to extract username from request.", - "key": "atn-token.handler", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#atnTokenHandler(io.helidon.security.util.TokenHandler)", - "type": "io.helidon.security.util.TokenHandler" - }, - { - "defaultValue": "true", - "description": "Configure whether to verify signatures.\n Signatures verification is enabled by default. You can configure the provider\n not to verify signatures.\n

\n Make sure your service is properly secured on network level and only\n accessible from a secure endpoint that provides the JWTs when signature verification\n is disabled. If signature verification is disabled, this service will accept ANY JWT", - "key": "atn-token.verify-signature", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#verifySignature(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Audience expected in inbound JWTs.", - "key": "atn-token.jwt-audience", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#expectedAudience(java.lang.String)" - }, - { - "description": "Configuration of outbound rules.", - "key": "sign-token", - "method": "io.helidon.security.providers.jwt.JwtProvider.Builder#outboundConfig(io.helidon.security.providers.common.OutboundConfig)", - "type": "io.helidon.security.providers.common.OutboundConfig" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.httpsign", - "types": [ - { - "annotatedType": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder", - "description": "HTTP header signature provider.", - "prefix": "http-signatures", - "type": "io.helidon.security.providers.httpsign.HttpSignProvider", - "producers": [ - "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#build()", - "io.helidon.security.providers.httpsign.HttpSignProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.AuthenticationProvider" - ], - "options": [ - { - "description": "Add a header that is validated on inbound requests. Provider may support more than\n one header to validate.", - "key": "headers", - "kind": "LIST", - "method": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#addAcceptHeader(io.helidon.security.providers.httpsign.HttpSignHeader)", - "type": "io.helidon.security.providers.httpsign.HttpSignHeader", - "allowedValues": [ - { - "description": "Creates (or validates) a \"Signature\" header.", - "value": "SIGNATURE" - }, - { - "description": "Creates (or validates) an \"Authorization\" header, that contains \"Signature\" as the\n beginning of its content (the rest of the header is the same as for #SIGNATURE.", - "value": "AUTHORIZATION" - }, - { - "description": "Custom provided using a io.helidon.security.util.TokenHandler.", - "value": "CUSTOM" - } - ] - }, - { - "defaultValue": "true", - "description": "Set whether the signature is optional. If set to true (default), this provider will\n SecurityResponse.SecurityStatus#ABSTAIN from this request if signature is not\n present. If set to false, this provider will SecurityResponse.SecurityStatus#FAILURE fail\n if signature is not present.", - "key": "optional", - "method": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#optional(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Enable support for Helidon versions before 3.0.0 (exclusive).\n

\n Until version 3.0.0 (exclusive) there was a trailing end of line added to the signed\n data.\n To be able to communicate cross versions, we must configure this when talking to older versions of Helidon.\n Default value is `false`. In Helidon 2.x, this switch exists as well and the default is `true`, to\n allow communication between versions as needed.", - "key": "backward-compatible-eol", - "method": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#backwardCompatibleEol(java.lang.Boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Add outbound targets to this builder.\n The targets are used to chose what to do for outbound communication.\n The targets should have OutboundTargetDefinition attached through\n OutboundTarget.Builder#customObject(Class, Object) to tell us how to sign\n the request.\n

\n The same can be done through configuration:\n

\n {\n  name = \"http-signatures\"\n  class = \"HttpSignProvider\"\n  http-signatures {\n      targets: [\n      {\n          name = \"service2\"\n          hosts = [\"localhost\"]\n          paths = [\"/service2/.*\"]\n\n          # This configures the OutboundTargetDefinition\n          signature {\n              key-id = \"service1\"\n              hmac.secret = \"${CLEAR=password}\"\n          }\n      }]\n  }\n }\n 
", - "key": "outbound", - "method": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#outbound(io.helidon.security.providers.common.OutboundConfig)", - "type": "io.helidon.security.providers.common.OutboundConfig" - }, - { - "description": "Add inbound configuration. This is used to validate signature and authenticate the\n party.\n

\n The same can be done through configuration:\n

\n {\n  name = \"http-signatures\"\n  class = \"HttpSignProvider\"\n  http-signatures {\n      inbound {\n          # This configures the InboundClientDefinition\n          keys: [\n          {\n              key-id = \"service1\"\n              hmac.secret = \"${CLEAR=password}\"\n          }]\n      }\n  }\n }\n 
", - "key": "inbound.keys", - "kind": "LIST", - "method": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#addInbound(io.helidon.security.providers.httpsign.InboundClientDefinition)", - "type": "io.helidon.security.providers.httpsign.InboundClientDefinition" - }, - { - "defaultValue": "helidon", - "description": "Realm to use for challenging inbound requests that do not have \"Authorization\" header\n in case header is HttpSignHeader#AUTHORIZATION and singatures are not optional.", - "key": "realm", - "method": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#realm(java.lang.String)" - }, - { - "description": "Override the default inbound required headers (e.g. headers that MUST be signed and\n headers that MUST be signed IF present).\n

\n Defaults:\n

    \n
  • get, head, delete methods: date, (request-target), host are mandatory; authorization if present (unless we are\n creating/validating the HttpSignHeader#AUTHORIZATION ourselves
  • \n
  • put, post: same as above, with addition of: content-length, content-type and digest if present\n
  • for other methods: date, (request-target)
  • \n
\n Note that this provider DOES NOT validate the \"Digest\" HTTP header, only the signature.", - "key": "sign-headers", - "kind": "LIST", - "method": "io.helidon.security.providers.httpsign.HttpSignProvider.Builder#inboundRequiredHeaders(io.helidon.security.providers.httpsign.SignedHeadersConfig)", - "type": "io.helidon.security.providers.httpsign.SignedHeadersConfig.HeadersConfig" - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder", - "type": "io.helidon.security.providers.httpsign.InboundClientDefinition", - "producers": [ - "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder#build()", - "io.helidon.security.providers.httpsign.InboundClientDefinition#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "The key id of this client to map to this signature validation configuration.", - "key": "key-id", - "method": "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder#keyId(java.lang.String)" - }, - { - "description": "For algorithms based on public/private key (such as rsa-sha256), this provides access to the public key of the client.", - "key": "public-key", - "method": "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder#publicKeyConfig(io.helidon.common.pki.Keys)", - "type": "io.helidon.common.pki.Keys" - }, - { - "description": "Helper method to configure a password-like secret (instead of byte based #hmacSecret(byte[]).\n The password is transformed to bytes with StandardCharsets#UTF_8 charset.", - "key": "hmac.secret", - "method": "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder#hmacSecret(java.lang.String)" - }, - { - "description": "The principal name of the client, defaults to keyId if not configured.", - "key": "principal-name", - "method": "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder#principalName(java.lang.String)" - }, - { - "description": "Algorithm of signature used by this client.\n Currently supported:\n
    \n
  • rsa-sha256 - asymmetric based on public/private keys
  • \n
  • hmac-sha256 - symmetric based on a shared secret
  • \n
", - "key": "algorithm", - "method": "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder#algorithm(java.lang.String)" - }, - { - "defaultValue": "SERVICE", - "description": "The type of principal we have authenticated (either user or service, defaults to service).", - "key": "principal-type", - "method": "io.helidon.security.providers.httpsign.InboundClientDefinition.Builder#subjectType(io.helidon.security.SubjectType)", - "type": "io.helidon.security.SubjectType", - "allowedValues": [ - { - "description": "", - "value": "USER" - }, - { - "description": "", - "value": "SERVICE" - } - ] - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.httpsign.SignedHeadersConfig.HeadersConfig", - "type": "io.helidon.security.providers.httpsign.SignedHeadersConfig.HeadersConfig", - "producers": [ - "io.helidon.security.providers.httpsign.SignedHeadersConfig.HeadersConfig#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "Headers that must be signed (and signature validation or creation should fail if not signed or present)", - "key": "always", - "kind": "LIST" - }, - { - "description": "Headers that must be signed if present in request.", - "key": "if-present", - "kind": "LIST" - }, - { - "description": "HTTP method this header configuration is bound to. If not present, it is considered default header configuration.", - "key": "method" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.oidc.common", - "types": [ - { - "annotatedType": "io.helidon.security.providers.oidc.common.OidcConfig.Builder", - "description": "Open ID Connect configuration", - "type": "io.helidon.security.providers.oidc.common.OidcConfig", - "inherits": [ - "io.helidon.security.providers.oidc.common.BaseBuilder" - ], - "producers": [ - "io.helidon.security.providers.oidc.common.OidcConfig.Builder#build()", - "io.helidon.security.providers.oidc.common.OidcConfig#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "defaultValue": "true", - "description": "Whether to encrypt state cookie created by this microservice.\n Defaults to `true`.", - "key": "cookie-encryption-state-enabled", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieEncryptionEnabledState(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Configurations of the tenants", - "key": "tenants", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#addTenantConfig(io.helidon.security.providers.oidc.common.TenantConfig)", - "type": "io.helidon.security.providers.oidc.common.TenantConfig" - }, - { - "defaultValue": "/oidc/redirect", - "description": "URI to register web server component on, used by the OIDC server to\n redirect authorization requests to after a user logs in or approves\n scopes.\n Note that usually the redirect URI configured here must be the\n same one as configured on OIDC server.\n\n

\n Defaults to {@value #DEFAULT_REDIRECT_URI}", - "key": "redirect-uri", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#redirectUri(java.lang.String)" - }, - { - "defaultValue": "false", - "description": "Can be set to `true` to force the use of relative URIs in all requests,\n regardless of the presence or absence of proxies or no-proxy lists. By default,\n requests that use the Proxy will have absolute URIs. Set this flag to `true`\n if the host is unable to accept absolute URIs.\n Defaults to {@value #DEFAULT_RELATIVE_URIS}.", - "key": "relative-uris", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#relativeUris(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Master password for encryption/decryption of cookies. This must be configured to the same value on each microservice\n using the cookie.", - "key": "cookie-encryption-password", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieEncryptionPassword(char[])", - "type": "char[]" - }, - { - "defaultValue": "/", - "description": "Path the cookie is valid for.\n Defaults to \"/\".", - "key": "cookie-path", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookiePath(java.lang.String)" - }, - { - "description": "When using cookie, used to set MaxAge attribute of the cookie, defining how long\n the cookie is valid.\n Not used by default.", - "key": "cookie-max-age-seconds", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieMaxAgeSeconds(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "false", - "description": "By default, the client should redirect to the identity server for the user to log in.\n This behavior can be overridden by setting redirect to false. When token is not present in the request, the client\n will not redirect and just return appropriate error response code.", - "key": "redirect", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#redirect(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Proxy host to use. When defined, triggers usage of proxy for HTTP requests.\n Setting to empty String has the same meaning as setting to null - disables proxy.", - "key": "proxy-host", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#proxyHost(java.lang.String)" - }, - { - "defaultValue": "false", - "description": "Whether to use a query parameter to send JWT token from application to this\n server.", - "key": "query-param-use", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#useParam(java.lang.Boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "HELIDON_TENANT", - "description": "The name of the cookie to use for the tenant name.\n Defaults to {@value #DEFAULT_TENANT_COOKIE_NAME}.", - "key": "cookie-name-tenant", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieTenantName(java.lang.String)" - }, - { - "defaultValue": "80", - "description": "Proxy port.\n Defaults to {@value DEFAULT_PROXY_PORT}", - "key": "proxy-port", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#proxyPort(int)", - "type": "java.lang.Integer" - }, - { - "description": "Domain the cookie is valid for.\n Not used by default.", - "key": "cookie-domain", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieDomain(java.lang.String)" - }, - { - "defaultValue": "http", - "description": "Proxy protocol to use when proxy is used.\n Defaults to {@value DEFAULT_PROXY_PROTOCOL}.", - "key": "proxy-protocol", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#proxyProtocol(java.lang.String)" - }, - { - "defaultValue": "false", - "description": "Whether to encrypt token cookie created by this microservice.\n Defaults to `false`.", - "key": "cookie-encryption-enabled", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieEncryptionEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "5", - "description": "Configure maximal number of redirects when redirecting to an OIDC provider within a single authentication\n attempt.\n

\n Defaults to {@value #DEFAULT_MAX_REDIRECTS}", - "key": "max-redirects", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#maxRedirects(int)", - "type": "java.lang.Integer" - }, - { - "description": "Name of the encryption configuration available through Security#encrypt(String, byte[]) and\n Security#decrypt(String, String).\n If configured and encryption is enabled for any cookie,\n Security MUST be configured in global or current `io.helidon.common.context.Context` (this\n is done automatically in Helidon MP).", - "key": "cookie-encryption-name", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieEncryptionName(java.lang.String)" - }, - { - "defaultValue": "true", - "description": "Whether id token signature check should be enabled.\n Signature check is enabled by default, and it is highly recommended to not change that.\n Change this setting only when you really know what you are doing, otherwise it could case security issues.", - "key": "id-token-signature-validation", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#idTokenSignatureValidation(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Full URI of this application that is visible from user browser.\n Used to redirect request back from identity server after successful login.", - "key": "frontend-uri", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#frontendUri(java.lang.String)" - }, - { - "defaultValue": "true", - "description": "Whether to check if current IP address matches the one access token was issued for.\n This check helps with cookie replay attack prevention.", - "key": "access-token-ip-check", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#accessTokenIpCheck(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "h_ra", - "description": "Configure the parameter used to store the number of attempts in redirect.\n

\n Defaults to {@value #DEFAULT_ATTEMPT_PARAM}", - "key": "redirect-attempt-param", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#redirectAttemptParam(java.lang.String)" - }, - { - "defaultValue": "LAX", - "description": "When using cookie, used to set the SameSite cookie value. Can be\n \"Strict\" or \"Lax\".", - "key": "cookie-same-site", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieSameSite(io.helidon.http.SetCookie.SameSite)", - "type": "io.helidon.http.SetCookie.SameSite", - "allowedValues": [ - { - "description": "", - "value": "LAX" - }, - { - "description": "", - "value": "STRICT" - }, - { - "description": "", - "value": "NONE" - } - ] - }, - { - "defaultValue": "accessToken", - "description": "Name of a query parameter that contains the JWT access token when parameter is used.", - "key": "query-param-name", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#paramName(java.lang.String)" - }, - { - "description": "Assign cross-origin resource sharing settings.", - "key": "cors", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#crossOriginConfig(io.helidon.cors.CrossOriginConfig)", - "type": "io.helidon.cors.CrossOriginConfig" - }, - { - "defaultValue": "false", - "description": "Force HTTPS for redirects to identity provider.\n Defaults to `false`.", - "key": "force-https-redirects", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#forceHttpsRedirects(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "id_token", - "description": "Name of a query parameter that contains the JWT id token when parameter is used.", - "key": "query-id-token-param-name", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#idTokenParamName(java.lang.String)" - }, - { - "defaultValue": "JSESSIONID_3", - "description": "The name of the cookie to use for the state storage.\n Defaults to {@value #DEFAULT_STATE_COOKIE_NAME}.", - "key": "cookie-name-state", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieNameState(java.lang.String)" - }, - { - "defaultValue": "true", - "description": "Whether to encrypt refresh token cookie created by this microservice.\n Defaults to `true`.", - "key": "cookie-encryption-refresh-enabled", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieEncryptionEnabledRefreshToken(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "When using cookie, if set to true, the HttpOnly attribute will be configured.\n Defaults to {@value OidcCookieHandler.Builder#DEFAULT_HTTP_ONLY}.", - "key": "cookie-http-only", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieHttpOnly(java.lang.Boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "A TokenHandler to\n process header containing a JWT.\n Default is \"Authorization\" header with a prefix \"bearer \".", - "key": "header-token", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#headerTokenHandler(io.helidon.security.util.TokenHandler)", - "type": "io.helidon.security.util.TokenHandler" - }, - { - "defaultValue": "true", - "description": "Whether to expect JWT in a header field.", - "key": "header-use", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#useHeader(java.lang.Boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "When using cookie, if set to true, the Secure attribute will be configured.\n Defaults to false.", - "key": "cookie-secure", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieSecure(java.lang.Boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Whether to encrypt tenant name cookie created by this microservice.\n Defaults to `true`.", - "key": "cookie-encryption-tenant-enabled", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieEncryptionEnabledTenantName(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Whether access token signature check should be enabled.\n Signature check is enabled by default, and it is highly recommended to not change that.\n Change this setting only when you really know what you are doing, otherwise it could case security issues.", - "key": "token-signature-validation", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#tokenSignatureValidation(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "JSESSIONID", - "description": "Name of the cookie to use.\n Defaults to {@value #DEFAULT_COOKIE_NAME}.", - "key": "cookie-name", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieName(java.lang.String)" - }, - { - "defaultValue": "h_tenant", - "description": "Name of a query parameter that contains the tenant name when the parameter is used.\n Defaults to #DEFAULT_TENANT_PARAM_NAME.", - "key": "query-param-tenant-name", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#paramTenantName(java.lang.String)" - }, - { - "defaultValue": "true", - "description": "Whether to encrypt id token cookie created by this microservice.\n Defaults to `true`.", - "key": "cookie-encryption-id-enabled", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieEncryptionEnabledIdToken(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "JSESSIONID_2", - "description": "Name of the cookie to use for id token.\n Defaults to {@value #DEFAULT_COOKIE_NAME}_2.\n\n This cookie is only used when logout is enabled, as otherwise it is not needed.\n Content of this cookie is encrypted.", - "key": "cookie-name-id-token", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieNameIdToken(java.lang.String)" - }, - { - "defaultValue": "JSESSIONID_3", - "description": "The name of the cookie to use for the refresh token.\n Defaults to {@value #DEFAULT_REFRESH_COOKIE_NAME}.", - "key": "cookie-name-refresh-token", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#cookieNameRefreshToken(java.lang.String)" - }, - { - "defaultValue": "true", - "description": "Whether to use cookie to store JWT between requests.\n Defaults to {@value #DEFAULT_COOKIE_USE}.", - "key": "cookie-use", - "method": "io.helidon.security.providers.oidc.common.OidcConfig.Builder#useCookie(java.lang.Boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.oidc.common.TenantConfig.Builder", - "description": "Open ID Connect tenant configuration", - "type": "io.helidon.security.providers.oidc.common.TenantConfig", - "inherits": [ - "io.helidon.security.providers.oidc.common.BaseBuilder" - ], - "producers": [ - "io.helidon.security.providers.oidc.common.TenantConfig.Builder#build()" - ], - "options": [ - { - "description": "Name of the tenant.", - "key": "name", - "method": "io.helidon.security.providers.oidc.common.TenantConfig.Builder#name(java.lang.String)", - "required": true - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.oidc.common.BaseBuilder", - "type": "io.helidon.security.providers.oidc.common.BaseBuilder", - "options": [ - { - "description": "URI of the identity server, base used to retrieve OIDC metadata.", - "key": "identity-uri", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#identityUri(java.net.URI)", - "type": "java.net.URI" - }, - { - "description": "Resource configuration for OIDC Metadata\n containing endpoints to various identity services, as well as information about the identity server.", - "key": "oidc-metadata.resource", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#oidcMetadata(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "description": "Endpoint to use to validate JWT.\n Either use this or set #signJwk(JwkKeys) or #signJwk(Resource).", - "key": "introspect-endpoint-uri", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#introspectEndpointUri(java.net.URI)", - "type": "java.net.URI" - }, - { - "defaultValue": "false", - "description": "Allow audience claim to be optional.", - "key": "optional-audience", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#optionalAudience(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Audience of issued tokens.", - "key": "audience", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#audience(java.lang.String)" - }, - { - "defaultValue": "30000", - "description": "Timeout of calls using web client.", - "key": "client-timeout-millis", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#clientTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "description": "A resource pointing to JWK with public keys of signing certificates used\n to validate JWT.", - "key": "sign-jwk.resource", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#signJwk(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "defaultValue": "openid", - "description": "Configure base scopes.\n By default, this is {@value #DEFAULT_BASE_SCOPES}.\n If scope has a qualifier, it must be used here.", - "key": "base-scopes", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#baseScopes(java.lang.String)" - }, - { - "defaultValue": "@default", - "description": "Configure one of the supported types of identity servers.\n\n If the type does not have an explicit mapping, a warning is logged and the default implementation is used.", - "key": "server-type", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#serverType(java.lang.String)" - }, - { - "description": "Issuer of issued tokens.", - "key": "issuer", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#issuer(java.lang.String)" - }, - { - "defaultValue": "false", - "description": "Configure audience claim check.", - "key": "check-audience", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#checkAudience(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Use JWK (a set of keys to validate signatures of JWT) to validate tokens.\n Use this method when you want to use default values for JWK or introspection endpoint URI.", - "key": "validate-jwt-with-jwk", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#validateJwtWithJwk(java.lang.Boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "If set to true, metadata will be loaded from default (well known)\n location, unless it is explicitly defined using oidc-metadata-resource. If set to false, it would not be loaded\n even if oidc-metadata-resource is not defined. In such a case all URIs must be explicitly defined (e.g.\n token-endpoint-uri).", - "key": "oidc-metadata-well-known", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#oidcMetadataWellKnown(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Client secret as generated by OIDC server.\n Used to authenticate this application with the server when requesting\n JWT based on a code.", - "key": "client-secret", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#clientSecret(java.lang.String)" - }, - { - "description": "Audience of the scope required by this application. This is prefixed to\n the scope name when requesting scopes from the identity server.\n Defaults to empty string.", - "key": "scope-audience", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#scopeAudience(java.lang.String)" - }, - { - "defaultValue": "CLIENT_SECRET_BASIC", - "description": "Type of authentication to use when invoking the token endpoint.\n Current supported options:\n

    \n
  • io.helidon.security.providers.oidc.common.OidcConfig.ClientAuthentication#CLIENT_SECRET_BASIC
  • \n
  • io.helidon.security.providers.oidc.common.OidcConfig.ClientAuthentication#CLIENT_SECRET_POST
  • \n
  • io.helidon.security.providers.oidc.common.OidcConfig.ClientAuthentication#NONE
  • \n
", - "key": "token-endpoint-auth", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#tokenEndpointAuthentication(io.helidon.security.providers.oidc.common.OidcConfig.ClientAuthentication)", - "type": "io.helidon.security.providers.oidc.common.OidcConfig.ClientAuthentication", - "allowedValues": [ - { - "description": "Clients that have received a client_secret value from the Authorization Server authenticate with the Authorization\n Server in accordance with Section 2.3.1 of OAuth 2.0 [RFC6749] using the HTTP Basic authentication scheme.\n This is the default client authentication.", - "value": "CLIENT_SECRET_BASIC" - }, - { - "description": "Clients that have received a client_secret value from the Authorization Server, authenticate with the Authorization\n Server in accordance with Section 2.3.1 of OAuth 2.0 [RFC6749] by including the Client Credentials in the request body.", - "value": "CLIENT_SECRET_POST" - }, - { - "description": "Clients that have received a client_secret value from the Authorization Server create a JWT using an HMAC SHA\n algorithm, such as HMAC SHA-256. The HMAC (Hash-based Message Authentication Code) is calculated using the octets of\n the UTF-8 representation of the client_secret as the shared key.\n The Client authenticates in accordance with JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and\n Authorization Grants [OAuth.JWT] and Assertion Framework for OAuth 2.0 Client Authentication and Authorization\n Grants [OAuth.Assertions].\n

\n The JWT MUST contain the following REQUIRED Claim Values and MAY contain the following\n OPTIONAL Claim Values.\n

\n Required:\n `iss, sub, aud, jti, exp`\n

\n Optional:\n `iat`", - "value": "CLIENT_SECRET_JWT" - }, - { - "description": "Clients that have registered a public key sign a JWT using that key. The Client authenticates in accordance with\n JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.JWT] and Assertion\n Framework for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.Assertions].\n

\n The JWT MUST contain the following REQUIRED Claim Values and MAY contain the following\n OPTIONAL Claim Values.\n

\n Required:\n `iss, sub, aud, jti, exp`\n

\n Optional:\n `iat`", - "value": "PRIVATE_KEY_JWT" - }, - { - "description": "The Client does not authenticate itself at the Token Endpoint, either because it uses only the Implicit Flow (and so\n does not use the Token Endpoint) or because it is a Public Client with no Client Secret or other authentication\n mechanism.", - "value": "NONE" - } - ] - }, - { - "description": "Client ID as generated by OIDC server.", - "key": "client-id", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#clientId(java.lang.String)" - }, - { - "description": "URI of an authorization endpoint used to redirect users to for logging-in.\n\n If not defined, it is obtained from #oidcMetadata(Resource), if that is not defined\n an attempt is made to use #identityUri(URI)/oauth2/v1/authorize.", - "key": "authorization-endpoint-uri", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#authorizationEndpointUri(java.net.URI)", - "type": "java.net.URI" - }, - { - "description": "URI of a token endpoint used to obtain a JWT based on the authentication\n code.\n If not defined, it is obtained from #oidcMetadata(Resource), if that is not defined\n an attempt is made to use #identityUri(URI)/oauth2/v1/token.", - "key": "token-endpoint-uri", - "method": "io.helidon.security.providers.oidc.common.BaseBuilder#tokenEndpointUri(java.net.URI)", - "type": "java.net.URI" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.idcs.mapper", - "types": [ - { - "annotatedType": "io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.Builder", - "description": "Multitenant IDCS role mapping provider", - "prefix": "idcs-role-mapper", - "type": "io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider", - "inherits": [ - "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder" - ], - "producers": [ - "io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.Builder#build()", - "io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.SubjectMappingProvider" - ], - "options": [ - { - "description": "Use explicit io.helidon.security.providers.common.EvictableCache for role caching.", - "key": "cache-config", - "method": "io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.Builder#cache(io.helidon.security.providers.common.EvictableCache>)", - "type": "io.helidon.security.providers.common.EvictableCache" - }, - { - "description": "Configure token handler for IDCS Application name.\n By default the header {@value IdcsMtRoleMapperProvider#IDCS_APP_HEADER} is used.", - "key": "idcs-app-name-handler", - "method": "io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.Builder#idcsAppNameTokenHandler(io.helidon.security.util.TokenHandler)", - "type": "io.helidon.security.util.TokenHandler" - }, - { - "description": "Configure token handler for IDCS Tenant ID.\n By default the header {@value IdcsMtRoleMapperProvider#IDCS_TENANT_HEADER} is used.", - "key": "idcs-tenant-handler", - "method": "io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.Builder#idcsTenantTokenHandler(io.helidon.security.util.TokenHandler)", - "type": "io.helidon.security.util.TokenHandler" - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder", - "type": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder", - "options": [ - { - "description": "Use explicit io.helidon.security.providers.oidc.common.OidcConfig instance, e.g. when using it also for OIDC\n provider.", - "key": "oidc-config", - "method": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder#oidcConfig(io.helidon.security.providers.oidc.common.OidcConfig)", - "type": "io.helidon.security.providers.oidc.common.OidcConfig" - }, - { - "defaultValue": "user", - "description": "Configure subject type to use when requesting roles from IDCS.\n Can be either #IDCS_SUBJECT_TYPE_USER or #IDCS_SUBJECT_TYPE_CLIENT.\n Defaults to #IDCS_SUBJECT_TYPE_USER.", - "key": "default-idcs-subject-type", - "method": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder#defaultIdcsSubjectType(java.lang.String)" - }, - { - "defaultValue": "USER", - "description": "Add a supported subject type.\n If none added, io.helidon.security.SubjectType#USER is used.\n If any added, only the ones added will be used (e.g. if you want to use\n both io.helidon.security.SubjectType#USER and io.helidon.security.SubjectType#SERVICE,\n both need to be added.", - "key": "subject-types", - "kind": "LIST", - "method": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder#addSubjectType(io.helidon.security.SubjectType)", - "type": "io.helidon.security.SubjectType", - "allowedValues": [ - { - "description": "", - "value": "USER" - }, - { - "description": "", - "value": "SERVICE" - } - ] - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider.Builder", - "description": "IDCS role mapping provider", - "prefix": "idcs-role-mapper", - "type": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider", - "inherits": [ - "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder" - ], - "producers": [ - "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider.Builder#build()", - "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.SubjectMappingProvider" - ], - "options": [ - { - "description": "Use explicit io.helidon.security.providers.common.EvictableCache for role caching.", - "key": "cache-config", - "method": "io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider.Builder#roleCache(io.helidon.security.providers.common.EvictableCache>)", - "type": "io.helidon.security.providers.common.EvictableCache" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.oidc", - "types": [ - { - "annotatedType": "io.helidon.security.providers.oidc.OidcProvider.Builder", - "description": "Open ID Connect security provider", - "prefix": "oidc", - "type": "io.helidon.security.providers.oidc.OidcProvider", - "producers": [ - "io.helidon.security.providers.oidc.OidcProvider.Builder#build()", - "io.helidon.security.providers.oidc.OidcProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.AuthenticationProvider", - "io.helidon.security.spi.SecurityProvider" - ], - "options": [ - { - "description": "Configuration of outbound rules.", - "key": "outbound-config", - "method": "io.helidon.security.providers.oidc.OidcProvider.Builder#outboundConfig(io.helidon.security.providers.common.OutboundConfig)", - "type": "io.helidon.security.providers.common.OutboundConfig", - "merge": true - }, - { - "description": "Configuration of OIDC (Open ID Connect).", - "key": "oidc-config", - "method": "io.helidon.security.providers.oidc.OidcProvider.Builder#oidcConfig(io.helidon.security.providers.oidc.common.OidcConfig)", - "type": "io.helidon.security.providers.oidc.common.OidcConfig", - "merge": true - }, - { - "defaultValue": "false", - "description": "Whether authentication is required.\n By default, request will fail if the authentication cannot be verified.\n If set to true, request will process and this provider will abstain.", - "key": "optional", - "method": "io.helidon.security.providers.oidc.OidcProvider.Builder#optional(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Claim `groups` from JWT will be used to automatically add\n groups to current subject (may be used with jakarta.annotation.security.RolesAllowed annotation).", - "key": "use-jwt-groups", - "method": "io.helidon.security.providers.oidc.OidcProvider.Builder#useJwtGroups(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Whether to propagate identity.", - "key": "propagate", - "method": "io.helidon.security.providers.oidc.OidcProvider.Builder#propagate(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.abac", - "types": [ - { - "annotatedType": "io.helidon.security.providers.abac.AbacProvider.Builder", - "description": "Attribute Based Access Control provider", - "prefix": "abac", - "type": "io.helidon.security.providers.abac.AbacProvider", - "producers": [ - "io.helidon.security.providers.abac.AbacProvider.Builder#build()", - "io.helidon.security.providers.abac.AbacProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.AuthorizationProvider" - ], - "options": [ - { - "defaultValue": "true", - "description": "Whether to fail if any attribute is left unvalidated.", - "key": "fail-on-unvalidated", - "method": "io.helidon.security.providers.abac.AbacProvider.Builder#failOnUnvalidated(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Whether to fail if NONE of the attributes is validated.", - "key": "fail-if-none-validated", - "method": "io.helidon.security.providers.abac.AbacProvider.Builder#failIfNoneValidated(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.common", - "types": [ - { - "annotatedType": "io.helidon.security.providers.common.OutboundConfig.Builder", - "description": "Outbound configuration for outbound security", - "type": "io.helidon.security.providers.common.OutboundConfig", - "producers": [ - "io.helidon.security.providers.common.OutboundConfig.Builder#build()", - "io.helidon.security.providers.common.OutboundConfig#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "Add a new target configuration.", - "key": "outbound", - "kind": "LIST", - "method": "io.helidon.security.providers.common.OutboundConfig.Builder#addTarget(io.helidon.security.providers.common.OutboundTarget)", - "type": "io.helidon.security.providers.common.OutboundTarget" - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.common.EvictableCache.Builder", - "type": "io.helidon.security.providers.common.EvictableCache", - "producers": [ - "io.helidon.security.providers.common.EvictableCache.Builder#build()", - "io.helidon.security.providers.common.EvictableCache#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "defaultValue": "true", - "description": "If the cacheEnabled is set to false, no caching will be done.\n Otherwise (default behavior) evictable caching will be used.", - "key": "cache-enabled", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#cacheEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Configure evictor to check if a record is still valid.\n This should be a fast way to check, as it is happening in a ConcurrentHashMap#forEachKey(long, Consumer).\n This is also called during all get and remove operations to only return valid records.", - "key": "evictor-class", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#evictor(java.util.function.BiFunction)", - "type": "java.lang.Class" - }, - { - "defaultValue": "10000", - "description": "Configure parallelism threshold.", - "key": "parallelism-threshold", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#parallelismThreshold(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "3600000", - "description": "Configure record timeout since last access.", - "key": "cache-timeout-millis", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#timeout(long, java.util.concurrent.TimeUnit)", - "type": "java.lang.Long" - }, - { - "defaultValue": "60000", - "description": "Delay from the creation of the cache to first eviction", - "key": "cache-evict-delay-millis", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#evictSchedule(long, long, java.util.concurrent.TimeUnit)", - "type": "java.lang.Long" - }, - { - "defaultValue": "300000", - "description": "How often to evict records", - "key": "cache-evict-period-millis", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#evictSchedule(long, long, java.util.concurrent.TimeUnit)", - "type": "java.lang.Long" - }, - { - "defaultValue": "100000", - "description": "Configure maximal cache size.", - "key": "max-size", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#maxSize(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "3600000", - "description": "Configure record timeout since its creation.", - "key": "cache-overall-timeout-millis", - "method": "io.helidon.security.providers.common.EvictableCache.Builder#overallTimeout(long, java.util.concurrent.TimeUnit)", - "type": "java.lang.Long" - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.common.OutboundTarget.Builder", - "type": "io.helidon.security.providers.common.OutboundTarget", - "producers": [ - "io.helidon.security.providers.common.OutboundTarget.Builder#build()", - "io.helidon.security.providers.common.OutboundTarget#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "Add supported paths for this target. May be called more than once to add more paths.\n The path is tested as is against called path, and also tested as a regular expression.", - "key": "paths", - "kind": "LIST", - "method": "io.helidon.security.providers.common.OutboundTarget.Builder#addPath(java.lang.String)" - }, - { - "description": "Add supported method for this target. May be called more than once to add more methods.\n The method is tested as is ignoring case against the used method.", - "key": "methods", - "kind": "LIST", - "method": "io.helidon.security.providers.common.OutboundTarget.Builder#addMethod(java.lang.String)" - }, - { - "description": "Add supported host for this target. May be called more than once to add more hosts.\n

\n Valid examples:\n

    \n
  • localhost\n
  • www.google.com\n
  • 127.0.0.1\n
  • *.oracle.com\n
  • 192.169.*.*\n
  • *.google.*\n
", - "key": "hosts", - "kind": "LIST", - "method": "io.helidon.security.providers.common.OutboundTarget.Builder#addHost(java.lang.String)" - }, - { - "description": "Configure the name of this outbound target.", - "key": "name", - "method": "io.helidon.security.providers.common.OutboundTarget.Builder#name(java.lang.String)", - "required": true - }, - { - "description": "Add supported transports for this target. May be called more than once to add more transports.\n

\n Valid examples:\n

    \n
  • http\n
  • https\n
\n There is no wildcard support", - "key": "transport", - "kind": "LIST", - "method": "io.helidon.security.providers.common.OutboundTarget.Builder#addTransport(java.lang.String)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.google.login", - "types": [ - { - "annotatedType": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder", - "description": "Google Authentication provider", - "prefix": "google-login", - "type": "io.helidon.security.providers.google.login.GoogleTokenProvider", - "producers": [ - "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#build()", - "io.helidon.security.providers.google.login.GoogleTokenProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.AuthenticationProvider" - ], - "options": [ - { - "defaultValue": "false", - "description": "If set to true, this provider will return io.helidon.security.SecurityResponse.SecurityStatus#ABSTAIN instead\n of failing in case of invalid request.", - "key": "optional", - "method": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#optional(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Set proxy host when talking to Google.", - "key": "proxy-host", - "method": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#proxyHost(java.lang.String)" - }, - { - "description": "Outbound configuration - a set of outbound targets that\n will have the token propagated.", - "key": "outbound", - "method": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#outboundConfig(io.helidon.security.providers.common.OutboundConfig)", - "type": "io.helidon.security.providers.common.OutboundConfig" - }, - { - "defaultValue": "helidon", - "description": "Set the authentication realm to build challenge, defaults to \"helidon\".", - "key": "realm", - "method": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#realm(java.lang.String)" - }, - { - "description": "Google application client id, to validate that the token was generated by Google for us.", - "key": "client-id", - "method": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#clientId(java.lang.String)" - }, - { - "defaultValue": "80", - "description": "Set proxy port when talking to Google.", - "key": "proxy-port", - "method": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#proxyPort(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "`Authorization` header with `bearer` prefix", - "description": "Token provider to extract Google access token from request, defaults to \"Authorization\" header with a \"bearer \" prefix.", - "key": "token", - "method": "io.helidon.security.providers.google.login.GoogleTokenProvider.Builder#tokenProvider(io.helidon.security.util.TokenHandler)", - "type": "io.helidon.security.util.TokenHandler" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.httpauth", - "types": [ - { - "annotatedType": "io.helidon.security.providers.httpauth.ConfigUserStore.ConfigUser", - "type": "io.helidon.security.providers.httpauth.ConfigUserStore.ConfigUser", - "producers": [ - "io.helidon.security.providers.httpauth.ConfigUserStore.ConfigUser#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "User's password", - "key": "password" - }, - { - "description": "List of roles the user is in", - "key": "roles", - "kind": "LIST" - }, - { - "description": "User's login", - "key": "login" - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.httpauth.HttpBasicAuthProvider.Builder", - "description": "HTTP Basic Authentication provider", - "prefix": "http-basic-auth", - "type": "io.helidon.security.providers.httpauth.HttpBasicAuthProvider", - "producers": [ - "io.helidon.security.providers.httpauth.HttpBasicAuthProvider.Builder#build()", - "io.helidon.security.providers.httpauth.HttpBasicAuthProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.AuthenticationProvider" - ], - "options": [ - { - "description": "Set user store to validate users.\n Removes any other stores added through #addUserStore(SecureUserStore).", - "key": "users", - "kind": "LIST", - "method": "io.helidon.security.providers.httpauth.HttpBasicAuthProvider.Builder#userStore(io.helidon.security.providers.httpauth.SecureUserStore)", - "type": "io.helidon.security.providers.httpauth.ConfigUserStore.ConfigUser" - }, - { - "defaultValue": "false", - "description": "Whether authentication is required.\n By default, request will fail if the authentication cannot be verified.\n If set to false, request will process and this provider will abstain.", - "key": "optional", - "method": "io.helidon.security.providers.httpauth.HttpBasicAuthProvider.Builder#optional(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Add a new outbound target to configure identity propagation or explicit username/password.", - "key": "outbound", - "kind": "LIST", - "method": "io.helidon.security.providers.httpauth.HttpBasicAuthProvider.Builder#addOutboundTarget(io.helidon.security.providers.common.OutboundTarget)", - "type": "io.helidon.security.providers.common.OutboundTarget" - }, - { - "defaultValue": "helidon", - "description": "Set the realm to use when challenging users.", - "key": "realm", - "method": "io.helidon.security.providers.httpauth.HttpBasicAuthProvider.Builder#realm(java.lang.String)" - }, - { - "defaultValue": "USER", - "description": "Principal type this provider extracts (and also propagates).", - "key": "principal-type", - "method": "io.helidon.security.providers.httpauth.HttpBasicAuthProvider.Builder#subjectType(io.helidon.security.SubjectType)", - "type": "io.helidon.security.SubjectType", - "allowedValues": [ - { - "description": "", - "value": "USER" - }, - { - "description": "", - "value": "SERVICE" - } - ] - } - ] - }, - { - "annotatedType": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder", - "description": "Http digest authentication security provider", - "prefix": "http-digest-auth", - "type": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider", - "producers": [ - "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#build()", - "io.helidon.security.providers.httpauth.HttpDigestAuthProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.AuthenticationProvider" - ], - "options": [ - { - "defaultValue": "NONE", - "description": "Only `AUTH` supported. If left empty, uses the legacy approach (older RFC version). `AUTH-INT` is not supported.", - "key": "qop", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#addDigestQop(io.helidon.security.providers.httpauth.HttpDigest.Qop)", - "type": "io.helidon.security.providers.httpauth.HttpDigest.Qop", - "allowedValues": [ - { - "description": "Legacy approach - used internally to parse headers. Do not use this option when\n building provider. If you want to support only legacy RFC, please use\n HttpDigestAuthProvider.Builder#noDigestQop().\n Only #AUTH is supported, as auth-int requires access to message body.", - "value": "NONE" - }, - { - "description": "QOP \"auth\" - stands for \"authentication\".", - "value": "AUTH" - } - ] - }, - { - "description": "Set user store to obtain passwords and roles based on logins.", - "key": "users", - "kind": "LIST", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#userStore(io.helidon.security.providers.httpauth.SecureUserStore)", - "type": "io.helidon.security.providers.httpauth.ConfigUserStore.ConfigUser" - }, - { - "defaultValue": "86400000", - "description": "How long will the nonce value be valid. When timed-out, browser will re-request username/password.", - "key": "nonce-timeout-millis", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#digestNonceTimeout(long, java.util.concurrent.TimeUnit)", - "type": "java.lang.Long" - }, - { - "description": "The nonce is encrypted using this secret - to make sure the nonce we get back was generated by us and to\n make sure we can safely time-out nonce values.\n This secret must be the same for all service instances (or all services that want to share the same authentication).\n Defaults to a random password - e.g. if deployed to multiple servers, the authentication WILL NOT WORK. You MUST\n provide your own password to work in a distributed environment with non-sticky load balancing.", - "key": "server-secret", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#digestServerSecret(char[])" - }, - { - "defaultValue": "false", - "description": "Whether authentication is required.\n By default, request will fail if the authentication cannot be verified.\n If set to false, request will process and this provider will abstain.", - "key": "optional", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#optional(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "Helidon", - "description": "Set the realm to use when challenging users.", - "key": "realm", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#realm(java.lang.String)" - }, - { - "defaultValue": "MD5", - "description": "Digest algorithm to use.", - "key": "algorithm", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#digestAlgorithm(io.helidon.security.providers.httpauth.HttpDigest.Algorithm)", - "type": "io.helidon.security.providers.httpauth.HttpDigest.Algorithm", - "allowedValues": [ - { - "description": "MD5 algorithm.", - "value": "MD5" - } - ] - }, - { - "defaultValue": "USER", - "description": "Principal type this provider extracts (and also propagates).", - "key": "principal-type", - "method": "io.helidon.security.providers.httpauth.HttpDigestAuthProvider.Builder#subjectType(io.helidon.security.SubjectType)", - "type": "io.helidon.security.SubjectType", - "allowedValues": [ - { - "description": "", - "value": "USER" - }, - { - "description": "", - "value": "SERVICE" - } - ] - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.security.providers.header", - "types": [ - { - "annotatedType": "io.helidon.security.providers.header.HeaderAtnProvider.Builder", - "description": "Security provider that extracts a username (or service name) from a header.", - "prefix": "header-atn", - "type": "io.helidon.security.providers.header.HeaderAtnProvider", - "producers": [ - "io.helidon.security.providers.header.HeaderAtnProvider.Builder#build()", - "io.helidon.security.providers.header.HeaderAtnProvider#create(io.helidon.common.config.Config)" - ], - "provides": [ - "io.helidon.security.spi.SecurityProvider", - "io.helidon.security.spi.AuthenticationProvider" - ], - "options": [ - { - "defaultValue": "true", - "description": "Whether to authenticate requests.", - "key": "authenticate", - "method": "io.helidon.security.providers.header.HeaderAtnProvider.Builder#authenticate(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Token handler to extract username from request.", - "key": "atn-token", - "method": "io.helidon.security.providers.header.HeaderAtnProvider.Builder#atnTokenHandler(io.helidon.security.util.TokenHandler)", - "type": "io.helidon.security.util.TokenHandler" - }, - { - "defaultValue": "false", - "description": "Whether authentication is required.\n By default, request will fail if the username cannot be extracted.\n If set to false, request will process and this provider will abstain.", - "key": "optional", - "method": "io.helidon.security.providers.header.HeaderAtnProvider.Builder#optional(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Whether to propagate identity.", - "key": "propagate", - "method": "io.helidon.security.providers.header.HeaderAtnProvider.Builder#propagate(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Configure outbound target for identity propagation.", - "key": "outbound", - "kind": "LIST", - "method": "io.helidon.security.providers.header.HeaderAtnProvider.Builder#addOutboundTarget(io.helidon.security.providers.common.OutboundTarget)", - "type": "io.helidon.security.providers.common.OutboundTarget" - }, - { - "description": "Token handler to create outbound headers to propagate identity.\n If not defined, #atnTokenHandler will be used.", - "key": "outbound-token", - "method": "io.helidon.security.providers.header.HeaderAtnProvider.Builder#outboundTokenHandler(io.helidon.security.util.TokenHandler)", - "type": "io.helidon.security.util.TokenHandler" - }, - { - "defaultValue": "USER", - "description": "Principal type this provider extracts (and also propagates).", - "key": "principal-type", - "method": "io.helidon.security.providers.header.HeaderAtnProvider.Builder#subjectType(io.helidon.security.SubjectType)", - "type": "io.helidon.security.SubjectType", - "allowedValues": [ - { - "description": "", - "value": "USER" - }, - { - "description": "", - "value": "SERVICE" - } - ] - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.openapi.ui", - "types": [ - { - "annotatedType": "io.helidon.openapi.ui.OpenApiUiConfig", - "type": "io.helidon.openapi.ui.OpenApiUi", - "producers": [ - "io.helidon.openapi.ui.OpenApiUiConfig#create(io.helidon.common.config.Config)", - "io.helidon.openapi.ui.OpenApiUiConfig#builder()", - "io.helidon.openapi.ui.OpenApiUi#create(io.helidon.openapi.ui.OpenApiUiConfig)" - ], - "options": [ - { - "description": "Full web context (not just the suffix).\n\n @return full web context path", - "key": "web-context", - "method": "io.helidon.openapi.ui.OpenApiUiConfig.Builder#webContext(java.lang.String)" - }, - { - "description": "Merges implementation-specific UI options.\n\n @return options for the UI to merge", - "key": "options", - "kind": "MAP", - "method": "io.helidon.openapi.ui.OpenApiUiConfig.Builder#options(java.lang.String)" - }, - { - "defaultValue": "true", - "description": "Sets whether the service should be enabled.\n\n @return `true` if enabled, `false` otherwise", - "key": "enabled", - "method": "io.helidon.openapi.ui.OpenApiUiConfig.Builder#isEnabled(java.lang.Boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.openapi", - "types": [ - { - "annotatedType": "io.helidon.openapi.OpenApiFeatureConfig", - "prefix": "openapi", - "type": "io.helidon.openapi.OpenApiFeature", - "standalone": true, - "producers": [ - "io.helidon.openapi.OpenApiFeatureConfig#create(io.helidon.common.config.Config)", - "io.helidon.openapi.OpenApiFeatureConfig#builder()", - "io.helidon.openapi.OpenApiFeature#create(io.helidon.openapi.OpenApiFeatureConfig)" - ], - "provides": [ - "io.helidon.webserver.spi.ServerFeatureProvider" - ], - "options": [ - { - "defaultValue": "/openapi", - "description": "Web context path for the OpenAPI endpoint.\n\n @return webContext to use", - "key": "web-context", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#webContext(java.lang.String)" - }, - { - "description": "CORS config.\n\n @return CORS config", - "key": "cors", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#cors(java.util.Optional)", - "type": "io.helidon.cors.CrossOriginConfig" - }, - { - "description": "List of sockets to register this feature on. If empty, it would get registered on all sockets.\n\n @return socket names to register on, defaults to empty (all available sockets)", - "key": "sockets", - "kind": "LIST", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#sockets(java.util.Set)" - }, - { - "description": "OpenAPI manager.\n\n @return the OpenAPI manager", - "key": "manager", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#manager(java.util.Optional>)", - "providerType": "io.helidon.openapi.spi.OpenApiManagerProvider", - "type": "io.helidon.openapi.OpenApiManager", - "provider": true - }, - { - "defaultValue": "90.0", - "description": "Weight of the OpenAPI feature. This is quite low, to be registered after routing.\n {@value io.helidon.openapi.OpenApiFeature#WEIGHT}.\n\n @return weight of the feature", - "key": "weight", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#weight(double)", - "type": "java.lang.Double" - }, - { - "defaultValue": "openapi", - "description": "Hints for role names the user is expected to be in.\n\n @return list of hints", - "key": "roles", - "kind": "LIST", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#roles(java.util.List)" - }, - { - "description": "Path of the static OpenAPI document file. Default types are `json`, `yaml`, and `yml`.\n\n @return location of the static OpenAPI document file", - "key": "static-file", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#staticFile(java.util.Optional)" - }, - { - "description": "OpenAPI services.\n\n @return the OpenAPI services", - "key": "services", - "kind": "LIST", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#services(java.util.List)", - "providerType": "io.helidon.openapi.spi.OpenApiServiceProvider", - "type": "io.helidon.openapi.OpenApiService", - "provider": true - }, - { - "defaultValue": "true", - "description": "Whether to allow anybody to access the endpoint.\n\n @return whether to permit access to metrics endpoint to anybody, defaults to `true`\n @see #roles()", - "key": "permit-all", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#permitAll(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Sets whether the feature should be enabled.\n\n @return `true` if enabled, `false` otherwise", - "key": "enabled", - "method": "io.helidon.openapi.OpenApiFeatureConfig.Builder#isEnabled(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.integrations.neo4j", - "types": [ - { - "annotatedType": "io.helidon.integrations.neo4j.Neo4j.Builder", - "type": "io.helidon.integrations.neo4j.Neo4j", - "producers": [ - "io.helidon.integrations.neo4j.Neo4j.Builder#build()", - "io.helidon.integrations.neo4j.Neo4j#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "Set trust strategy.", - "key": "trust-strategy", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#trustStrategy(io.helidon.integrations.neo4j.Neo4j.Builder.TrustStrategy)", - "type": "io.helidon.integrations.neo4j.Neo4j.Builder.TrustStrategy", - "allowedValues": [ - { - "description": "Trust all.", - "value": "TRUST_ALL_CERTIFICATES" - }, - { - "description": "Trust custom certificates.", - "value": "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES" - }, - { - "description": "Trust system CA.", - "value": "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES" - } - ] - }, - { - "defaultValue": "PT1MS", - "description": "Set idle time.", - "key": "idle-time-before-connection-test", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#idleTimeBeforeConnectionTest(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "description": "Set certificate path.", - "key": "certificate", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#certificate(java.nio.file.Path)", - "type": "java.nio.file.Path" - }, - { - "description": "Create uri.", - "key": "uri", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#uri(java.lang.String)" - }, - { - "description": "Enable metrics.", - "key": "metrics-enabled", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#metricsEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Enable hostname verification.", - "key": "hostname-verification-enabled", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#hostnameVerificationEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "PT1M", - "description": "Set connection acquisition timeout.", - "key": "connection-acquisition-timeout", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#connectionAcquisitionTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "100", - "description": "Set pool size.", - "key": "max-connection-pool-size", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#maxConnectionPoolSize(int)", - "type": "java.lang.Integer" - }, - { - "description": "Create password.", - "key": "password", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#password(java.lang.String)" - }, - { - "description": "Enable encrypted field.", - "key": "encrypted", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#encrypted(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Enable authentication.", - "key": "authentication-enabled", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#authenticationEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Enable log leaked sessions.", - "key": "log-leaked-sessions", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#logLeakedSessions(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Create username.", - "key": "username", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#username(java.lang.String)" - }, - { - "defaultValue": "PT5H", - "description": "Set max life time.", - "key": "max-connection-lifetime", - "method": "io.helidon.integrations.neo4j.Neo4j.Builder#maxConnectionLifetime(java.time.Duration)", - "type": "java.time.Duration" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.integrations.openapi.ui", - "types": [ - { - "annotatedType": "io.helidon.integrations.openapi.ui.OpenApiUiConfig", - "type": "io.helidon.integrations.openapi.ui.OpenApiUi", - "producers": [ - "io.helidon.integrations.openapi.ui.OpenApiUiConfig#create(io.helidon.common.config.Config)", - "io.helidon.integrations.openapi.ui.OpenApiUiConfig#builder()", - "io.helidon.integrations.openapi.ui.OpenApiUi#create(io.helidon.integrations.openapi.ui.OpenApiUiConfig)" - ], - "options": [ - { - "description": "Full web context (not just the suffix).\n\n @return full web context path", - "key": "web-context", - "method": "io.helidon.integrations.openapi.ui.OpenApiUiConfig.Builder#webContext(java.util.Optional)" - }, - { - "description": "Merges implementation-specific UI options.\n\n @return options for the UI to merge", - "key": "options", - "kind": "MAP", - "method": "io.helidon.integrations.openapi.ui.OpenApiUiConfig.Builder#options(java.util.Map)" - }, - { - "defaultValue": "true", - "description": "Sets whether the service should be enabled.\n\n @return `true` if enabled, `false` otherwise", - "key": "enabled", - "method": "io.helidon.integrations.openapi.ui.OpenApiUiConfig.Builder#isEnabled(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.integrations.micrometer", - "types": [ - { - "annotatedType": "io.helidon.integrations.micrometer.MicrometerFeature.Builder", - "prefix": "micrometer", - "type": "io.helidon.integrations.micrometer.MicrometerFeature", - "inherits": [ - "io.helidon.webserver.servicecommon.HelidonFeatureSupport.Builder" - ], - "producers": [ - "io.helidon.integrations.micrometer.MicrometerFeature.Builder#build()", - "io.helidon.integrations.micrometer.MicrometerFeature#create(io.helidon.common.config.Config)" - ], - "options": [] - } - ] - } -] -[ - { - "module": "io.helidon.integrations.oci.metrics", - "types": [ - { - "annotatedType": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder", - "type": "io.helidon.integrations.oci.metrics.OciMetricsSupport", - "producers": [ - "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#build()" - ], - "options": [ - { - "defaultValue": "60", - "description": "Sets the delay interval between metric posting\n (defaults to {@value #DEFAULT_SCHEDULER_DELAY}).", - "key": "delay", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#delay(long)", - "type": "java.lang.Long" - }, - { - "description": "Sets the resource group.", - "key": "resource-group", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#resourceGroup(java.lang.String)" - }, - { - "description": "Sets the compartment ID.", - "key": "compartment-id", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#compartmentId(java.lang.String)" - }, - { - "defaultValue": "1", - "description": "Sets the delay interval if metrics are posted in batches\n (defaults to {@value #DEFAULT_BATCH_DELAY}).", - "key": "batch-delay", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#batchDelay(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "50", - "description": "Sets the maximum no. of metrics to send in a batch\n (defaults to {@value #DEFAULT_BATCH_SIZE}).", - "key": "batch-size", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#batchSize(int)", - "type": "java.lang.Integer" - }, - { - "description": "Sets the namespace.", - "key": "namespace", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#namespace(java.lang.String)" - }, - { - "defaultValue": "All scopes", - "description": "Sets which metrics scopes (e.g., base, vendor, application) should be sent to OCI.\n

\n If this method is never invoked, defaults to all scopes.\n

", - "key": "scopes", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#scopes(java.lang.String[])", - "type": "java.lang.String[]" - }, - { - "defaultValue": "TimeUnit.SECONDS", - "description": "Sets the time unit applied to the initial delay and delay values (defaults to `TimeUnit.SECONDS`).", - "key": "scheduling-time-unit", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#schedulingTimeUnit(java.util.concurrent.TimeUnit)", - "type": "java.util.concurrent.TimeUnit", - "allowedValues": [ - { - "description": "", - "value": "NANOSECONDS" - }, - { - "description": "", - "value": "MICROSECONDS" - }, - { - "description": "", - "value": "MILLISECONDS" - }, - { - "description": "", - "value": "SECONDS" - }, - { - "description": "", - "value": "MINUTES" - }, - { - "description": "", - "value": "HOURS" - }, - { - "description": "", - "value": "DAYS" - } - ] - }, - { - "defaultValue": "true", - "description": "Sets whether the description should be enabled or not.\n

\n Defaults to `true`.\n

", - "key": "description-enabled", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#descriptionEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "1", - "description": "Sets the initial delay before metrics are sent to OCI\n (defaults to {@value #DEFAULT_SCHEDULER_INITIAL_DELAY}).", - "key": "initial-delay", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#initialDelay(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "true", - "description": "Sets whether metrics transmission to OCI is enabled.\n

\n Defaults to `true`.\n

", - "key": "enabled", - "method": "io.helidon.integrations.oci.metrics.OciMetricsSupport.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.integrations.oci.sdk.runtime", - "types": [ - { - "annotatedType": "io.helidon.integrations.oci.sdk.runtime.OciConfig", - "prefix": "oci", - "type": "io.helidon.integrations.oci.sdk.runtime.OciConfig", - "standalone": true, - "producers": [ - "io.helidon.integrations.oci.sdk.runtime.OciConfig#create(io.helidon.common.config.Config)", - "io.helidon.integrations.oci.sdk.runtime.OciConfig#builder()" - ], - "options": [ - { - "description": "The OCI region.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #simpleConfigIsPresent().\n When it is present, either this property or com.oracle.bmc.auth.RegionProvider must be provide a value in order\n to set the {@linkplain com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider#getRegion()}.\n\n @return the OCI region", - "key": "auth.region", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authRegion(java.lang.String)" - }, - { - "description": "The OCI tenant id.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #simpleConfigIsPresent().\n When it is present, this property must be provided in order to set the\n {@linkplain com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider#getTenantId()}.\n\n @return the OCI tenant id", - "key": "auth.tenant-id", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authTenantId(java.lang.String)" - }, - { - "description": "The OCI configuration profile path.\n

\n This configuration property has an effect only when `config-file` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #fileConfigIsPresent().\n When it is present, this property must also be present and then the\n {@linkplain com.oracle.bmc.ConfigFileReader#parse(String)}\n method will be passed this value. It is expected to be passed with a\n valid OCI configuration file path.\n\n @return the OCI configuration profile path", - "key": "config.path", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#configPath(java.lang.String)" - }, - { - "description": "The list of authentication strategies that will be attempted by\n com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider when one is\n called for. This is only used if #authStrategy() is not present.\n\n

    \n
  • `auto` - if present in the list, or if no value\n for this property exists.
  • \n
  • `config` - the\n com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider\n will be used, customized with other configuration\n properties described here.
  • \n
  • `config-file` - the\n com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider\n will be used, customized with other configuration\n properties described here.
  • \n
  • `instance-principals` - the\n com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider\n will be used.
  • \n
  • `resource-principal` - the\n com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider\n will be used.
  • \n
\n

\n If there are more than one strategy descriptors defined, the\n first one that is deemed to be available/suitable will be used and all others will be ignored.\n\n @return the list of authentication strategies that will be applied, defaulting to `auto`\n @see io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.AuthStrategy", - "key": "auth-strategies", - "kind": "LIST", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authStrategies(java.lang.String)", - "allowedValues": [ - { - "description": "auto select first applicable", - "value": "auto" - }, - { - "description": "simple authentication provider", - "value": "config" - }, - { - "description": "config file authentication provider", - "value": "config-file" - }, - { - "description": "instance principals authentication provider", - "value": "instance-principals" - }, - { - "description": "resource principal authentication provider", - "value": "resource-principal" - } - ] - }, - { - "defaultValue": "DEFAULT", - "description": "The OCI configuration/auth profile name.\n

\n This configuration property has an effect only when `config-file` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #fileConfigIsPresent().\n When it is present, this property may also be optionally provided in order to override the default\n {@value #DEFAULT_PROFILE_NAME}.\n\n @return the optional OCI configuration/auth profile name", - "key": "config.profile", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#configProfile(java.lang.String)" - }, - { - "defaultValue": "oci_api_key.pem", - "description": "The OCI authentication key file.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #simpleConfigIsPresent().\n When it is present, this property must be provided in order to set the\n {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPrivateKey()}. This file must exist in the\n `user.home` directory. Alternatively, this property can be set using either #authPrivateKey() or\n using #authPrivateKeyPath().\n\n @return the OCI authentication key file", - "key": "auth.keyFile", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authKeyFile(java.lang.String)" - }, - { - "description": "The OCI authentication private key.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #simpleConfigIsPresent().\n When it is present, this property must be provided in order to set the\n {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPrivateKey()}. Alternatively, this property\n can be set using either #authKeyFile() residing in the `user.home` directory, or using\n #authPrivateKeyPath().\n\n @return the OCI authentication private key", - "key": "auth.private-key", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authPrivateKey(char[])", - "type": "char[]" - }, - { - "description": "The OCI user id.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies().\n When it is present, this property must be provided in order to set the\n {@linkplain com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider#getUserId()}.\n\n @return the OCI user id", - "key": "auth.user-id", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authUserId(java.lang.String)" - }, - { - "defaultValue": "169.254.169.254", - "description": "The OCI IMDS hostname.\n

\n This configuration property is used to identify the metadata service url.\n\n @return the OCI IMDS hostname", - "key": "imds.hostname", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#imdsHostName(java.lang.String)" - }, - { - "description": "The OCI authentication fingerprint.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #simpleConfigIsPresent().\n When it is present, this property must be provided in order to set the API signing key's fingerprint.\n See {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getFingerprint()} for more details.\n\n @return the OCI authentication fingerprint", - "key": "auth.fingerprint", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authFingerprint(java.lang.String)" - }, - { - "description": "The singular authentication strategy to apply. This will be preferred over #authStrategies() if both are\n present.\n\n @return the singular authentication strategy to be applied", - "key": "auth-strategy", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authStrategy(java.lang.String)", - "allowedValues": [ - { - "description": "auto select first applicable", - "value": "auto" - }, - { - "description": "simple authentication provider", - "value": "config" - }, - { - "description": "config file authentication provider", - "value": "config-file" - }, - { - "description": "instance principals authentication provider", - "value": "instance-principals" - }, - { - "description": "resource principals authentication provider", - "value": "resource-principal" - } - ] - }, - { - "description": "The OCI authentication key file path.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #simpleConfigIsPresent().\n When it is present, this property must be provided in order to set the\n {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPrivateKey()}. This file path is\n an alternative for using #authKeyFile() where the file must exist in the `user.home` directory.\n Alternatively, this property can be set using #authPrivateKey().\n\n @return the OCI authentication key file path", - "key": "auth.private-key-path", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authPrivateKeyPath(java.lang.String)" - }, - { - "description": "The OCI authentication passphrase.\n

\n This configuration property has an effect only when `config` is, explicitly or implicitly,\n present in the value for the #authStrategies(). This is also known as #simpleConfigIsPresent().\n When it is present, this property must be provided in order to set the\n {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPassphraseCharacters()}.\n\n @return the OCI authentication passphrase", - "key": "auth.passphrase", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#authPassphrase(char[])", - "type": "char[]" - }, - { - "defaultValue": "PT0.1S", - "description": "The OCI IMDS connection timeout. This is used to auto-detect availability.\n

\n This configuration property is used when attempting to connect to the metadata service.\n\n @return the OCI IMDS connection timeout\n @see OciAvailability", - "key": "imds.timeout.milliseconds", - "method": "io.helidon.integrations.oci.sdk.runtime.OciConfig.Builder#imdsTimeout(java.time.Duration)", - "type": "java.time.Duration" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.websocket", - "types": [ - { - "annotatedType": "io.helidon.webserver.websocket.WsConfig", - "type": "io.helidon.webserver.websocket.WsConfig", - "producers": [ - "io.helidon.webserver.websocket.WsConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.websocket.WsConfig#builder()" - ], - "provides": [ - "io.helidon.webserver.spi.ProtocolConfig" - ], - "options": [ - { - "description": "WebSocket origins.\n\n @return origins", - "key": "origins", - "kind": "LIST", - "method": "io.helidon.webserver.websocket.WsConfig.Builder#origins(java.util.Set)" - }, - { - "defaultValue": "websocket", - "description": "Name of this configuration.\n\n @return configuration name", - "key": "name", - "method": "io.helidon.webserver.websocket.WsConfig.Builder#name(java.lang.String)" - }, - { - "defaultValue": "1048576", - "description": "Max WebSocket frame size supported by the server on a read operation.\n Default is 1 MB.\n\n @return max frame size to read", - "key": "max-frame-length", - "method": "io.helidon.webserver.websocket.WsConfig.Builder#maxFrameLength(int)", - "type": "java.lang.Integer" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.context", - "types": [ - { - "annotatedType": "io.helidon.webserver.context.ContextFeatureConfig", - "prefix": "context", - "type": "io.helidon.webserver.context.ContextFeature", - "producers": [ - "io.helidon.webserver.context.ContextFeatureConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.context.ContextFeatureConfig#builder()", - "io.helidon.webserver.context.ContextFeature#create(io.helidon.webserver.context.ContextFeatureConfig)" - ], - "provides": [ - "io.helidon.webserver.spi.ServerFeatureProvider" - ], - "options": [ - { - "description": "List of sockets to register this feature on. If empty, it would get registered on all sockets.\n\n @return socket names to register on, defaults to empty (all available sockets)", - "key": "sockets", - "kind": "LIST", - "method": "io.helidon.webserver.context.ContextFeatureConfig.Builder#sockets(java.util.Set)" - }, - { - "defaultValue": "1100.0", - "description": "Weight of the context feature. As it is used by other features, the default is quite high:\n {@value io.helidon.webserver.context.ContextFeature#WEIGHT}.\n\n @return weight of the feature", - "key": "weight", - "method": "io.helidon.webserver.context.ContextFeatureConfig.Builder#weight(double)", - "type": "java.lang.Double" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.accesslog", - "types": [ - { - "annotatedType": "io.helidon.webserver.accesslog.AccessLogConfig", - "prefix": "access-log", - "type": "io.helidon.webserver.accesslog.AccessLogFeature", - "producers": [ - "io.helidon.webserver.accesslog.AccessLogConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.accesslog.AccessLogConfig#builder()", - "io.helidon.webserver.accesslog.AccessLogFeature#create(io.helidon.webserver.accesslog.AccessLogConfig)" - ], - "provides": [ - "io.helidon.webserver.spi.ServerFeatureProvider" - ], - "options": [ - { - "description": "List of sockets to register this feature on. If empty, it would get registered on all sockets.\n The logger used will have the expected logger with a suffix of the socket name.\n\n @return socket names to register on, defaults to empty (all available sockets)", - "key": "sockets", - "kind": "LIST", - "method": "io.helidon.webserver.accesslog.AccessLogConfig.Builder#sockets(java.util.Set)" - }, - { - "defaultValue": "1000.0", - "description": "Weight of the access log feature. We need to log access for anything happening on the server, so weight is high:\n {@value io.helidon.webserver.accesslog.AccessLogFeature#WEIGHT}.\n\n @return weight of the feature", - "key": "weight", - "method": "io.helidon.webserver.accesslog.AccessLogConfig.Builder#weight(double)", - "type": "java.lang.Double" - }, - { - "defaultValue": "io.helidon.webserver.AccessLog", - "description": "Name of the logger used to obtain access log logger from System#getLogger(String).\n Defaults to {@value AccessLogFeature#DEFAULT_LOGGER_NAME}.\n\n @return name of the logger to use", - "key": "logger-name", - "method": "io.helidon.webserver.accesslog.AccessLogConfig.Builder#loggerName(java.lang.String)" - }, - { - "description": "The format for log entries (similar to the Apache `LogFormat`).\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Log format elements
%hIP address of the remote hostHostLogEntry
%lThe client identity. This is always undefined in Helidon.UserIdLogEntry
%uUser ID as asserted by Helidon Security.UserLogEntry
%tThe timestampTimestampLogEntry
%rThe request line (`\"GET /favicon.ico HTTP/1.0\"`)RequestLineLogEntry
%sThe status code returned to the clientStatusLogEntry
%bThe entity size in bytesSizeLogEntry
%DThe time taken in microseconds (start of request until last byte written)TimeTakenLogEntry
%TThe time taken in seconds (start of request until last byte written), integerTimeTakenLogEntry
%{header-name}iValue of header `header-name`HeaderLogEntry
\n\n @return format string, such as `%h %l %u %t %r %b %{Referer`i}", - "key": "format", - "method": "io.helidon.webserver.accesslog.AccessLogConfig.Builder#format(java.util.Optional)" - }, - { - "defaultValue": "true", - "description": "Whether this feature will be enabled.\n\n @return whether enabled", - "key": "enabled", - "method": "io.helidon.webserver.accesslog.AccessLogConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.grpc", - "types": [ - { - "annotatedType": "io.helidon.webserver.grpc.GrpcConfig", - "type": "io.helidon.webserver.grpc.GrpcConfig", - "producers": [ - "io.helidon.webserver.grpc.GrpcConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.grpc.GrpcConfig#builder()" - ], - "provides": [ - "io.helidon.webserver.spi.ProtocolConfig" - ], - "options": [] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.security", - "types": [ - { - "annotatedType": "io.helidon.webserver.security.SecurityHandlerConfig", - "type": "io.helidon.webserver.security.SecurityHandler", - "producers": [ - "io.helidon.webserver.security.SecurityHandlerConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.security.SecurityHandlerConfig#builder()", - "io.helidon.webserver.security.SecurityHandler#create(io.helidon.webserver.security.SecurityHandlerConfig)" - ], - "options": [ - { - "description": "If called, authentication failure will not abort request and will continue as anonymous (defaults to false).\n\n @return whether authn is optional", - "key": "authentication-optional", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#authenticationOptional(java.util.Optional)", - "type": "java.lang.Boolean" - }, - { - "description": "An array of allowed roles for this path - must have a security provider supporting roles (either authentication\n or authorization provider).\n This method enables authentication and authorization (you can disable them again by calling\n SecurityHandler#skipAuthorization()\n and #authenticationOptional() if needed).\n\n @return if subject is any of these roles, allow access", - "key": "roles-allowed", - "kind": "LIST", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#rolesAllowed(java.util.Set)" - }, - { - "description": "If called, request will go through authentication process - defaults to false (even if authorize is true).\n\n @return whether to authenticate or not", - "key": "authenticate", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#authenticate(java.util.Optional)", - "type": "java.lang.Boolean" - }, - { - "description": "Use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is\n permitted).\n Will enable authorization.\n\n @return name of authorizer as configured in io.helidon.security.Security", - "key": "authorizer", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#authorizer(java.util.Optional)" - }, - { - "description": "List of sockets this configuration should be applied to.\n If empty, the configuration is applied to all configured sockets.\n\n @return list of sockets", - "key": "sockets", - "kind": "LIST", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#sockets(java.util.List)" - }, - { - "description": "Override for event-type, defaults to {@value SecurityHandler#DEFAULT_AUDIT_EVENT_TYPE}.\n\n @return audit event type to use", - "key": "audit-event-type", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#auditEventType(java.util.Optional)" - }, - { - "description": "Whether to audit this request - defaults to false, if enabled, request is audited with event type \"request\".\n\n @return whether to audit", - "key": "audit", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#audit(java.util.Optional)", - "type": "java.lang.Boolean" - }, - { - "description": "Override for audit message format, defaults to {@value SecurityHandler#DEFAULT_AUDIT_MESSAGE_FORMAT}.\n\n @return audit message format to use", - "key": "audit-message-format", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#auditMessageFormat(java.util.Optional)" - }, - { - "description": "Use a named authenticator (as supported by security - if not defined, default authenticator is used).\n Will enable authentication.\n\n @return name of authenticator as configured in io.helidon.security.Security", - "key": "authenticator", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#authenticator(java.util.Optional)" - }, - { - "description": "Enable authorization for this route.\n\n @return whether to authorize", - "key": "authorize", - "method": "io.helidon.webserver.security.SecurityHandlerConfig.Builder#authorize(java.util.Optional)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.security.PathsConfig", - "type": "io.helidon.webserver.security.PathsConfig", - "producers": [ - "io.helidon.webserver.security.PathsConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.security.PathsConfig#builder()" - ], - "options": [ - { - "description": "", - "key": "path", - "method": "io.helidon.webserver.security.PathsConfig.Builder#path(java.lang.String)" - }, - { - "description": "", - "key": "handler", - "method": "io.helidon.webserver.security.PathsConfig.Builder#handler(io.helidon.webserver.security.SecurityHandler)", - "type": "io.helidon.webserver.security.SecurityHandler", - "merge": true - }, - { - "defaultValue": "@default", - "description": "", - "key": "sockets", - "kind": "LIST", - "method": "io.helidon.webserver.security.PathsConfig.Builder#sockets(java.util.List)" - }, - { - "description": "", - "key": "methods", - "kind": "LIST", - "method": "io.helidon.webserver.security.PathsConfig.Builder#methods(java.util.List)", - "type": "io.helidon.http.Method" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.security.SecurityFeatureConfig", - "prefix": "security", - "type": "io.helidon.webserver.security.SecurityFeature", - "producers": [ - "io.helidon.webserver.security.SecurityFeatureConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.security.SecurityFeatureConfig#builder()", - "io.helidon.webserver.security.SecurityFeature#create(io.helidon.webserver.security.SecurityFeatureConfig)" - ], - "provides": [ - "io.helidon.webserver.spi.ServerFeatureProvider" - ], - "options": [ - { - "description": "Configuration for webserver paths.\n\n @return path configuration", - "key": "paths", - "kind": "LIST", - "method": "io.helidon.webserver.security.SecurityFeatureConfig.Builder#paths(java.util.List)", - "type": "io.helidon.webserver.security.PathsConfig" - }, - { - "defaultValue": "800.0", - "description": "Weight of the security feature. Value is:\n {@value io.helidon.webserver.security.SecurityFeature#WEIGHT}.\n\n @return weight of the feature", - "key": "weight", - "method": "io.helidon.webserver.security.SecurityFeatureConfig.Builder#weight(double)", - "type": "java.lang.Double" - }, - { - "defaultValue": "SecurityHandler.create()", - "description": "The default security handler.\n\n @return security handler defaults", - "key": "defaults", - "method": "io.helidon.webserver.security.SecurityFeatureConfig.Builder#defaults(io.helidon.webserver.security.SecurityHandler)", - "type": "io.helidon.webserver.security.SecurityHandler" - }, - { - "description": "Security associated with this feature.\n If not specified here, the feature uses security registered with\n io.helidon.common.context.Contexts#globalContext(), if not found, it creates a new\n instance from root of configuration (using `security` key).\n

\n This configuration allows usage of a different security instance for a specific security feature setup.\n\n @return security instance to be used to handle security in this feature configuration", - "key": "security", - "method": "io.helidon.webserver.security.SecurityFeatureConfig.Builder#security(io.helidon.security.Security)", - "type": "io.helidon.security.Security" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.observe.metrics", - "types": [ - { - "annotatedType": "io.helidon.webserver.observe.metrics.MetricsObserverConfig", - "prefix": "metrics", - "type": "io.helidon.webserver.observe.metrics.MetricsObserver", - "standalone": true, - "producers": [ - "io.helidon.webserver.observe.metrics.MetricsObserverConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.metrics.MetricsObserverConfig#builder()", - "io.helidon.webserver.observe.metrics.MetricsObserver#create(io.helidon.webserver.observe.metrics.MetricsObserverConfig)" - ], - "provides": [ - "io.helidon.webserver.observe.spi.ObserveProvider" - ], - "options": [ - { - "defaultValue": "metrics", - "description": "", - "key": "endpoint", - "method": "io.helidon.webserver.observe.metrics.MetricsObserverConfig.Builder#endpoint(java.lang.String)" - }, - { - "defaultValue": "@io.helidon.metrics.api.MetricsConfig@.create()", - "description": "Assigns `MetricsSettings` which will be used in creating the `MetricsSupport` instance at build-time.\n\n @return the metrics settings to assign for use in building the `MetricsSupport` instance", - "key": "metrics-config", - "method": "io.helidon.webserver.observe.metrics.MetricsObserverConfig.Builder#metricsConfig(io.helidon.metrics.api.MetricsConfig)", - "type": "io.helidon.metrics.api.MetricsConfig", - "merge": true - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.observe.tracing", - "types": [ - { - "annotatedType": "io.helidon.webserver.observe.tracing.TracingObserverConfig", - "type": "io.helidon.webserver.observe.tracing.TracingObserver", - "producers": [ - "io.helidon.webserver.observe.tracing.TracingObserverConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.tracing.TracingObserverConfig#builder()", - "io.helidon.webserver.observe.tracing.TracingObserver#create(io.helidon.webserver.observe.tracing.TracingObserverConfig)" - ], - "provides": [ - "io.helidon.webserver.observe.spi.ObserveProvider" - ], - "options": [ - { - "defaultValue": "TracingConfig.ENABLED", - "description": "Use the provided configuration as a default for any request.\n\n @return default web server tracing configuration", - "key": "env-config", - "method": "io.helidon.webserver.observe.tracing.TracingObserverConfig.Builder#envConfig(io.helidon.tracing.config.TracingConfig)", - "type": "io.helidon.tracing.config.TracingConfig", - "merge": true - }, - { - "defaultValue": "new @java.util.ArrayList@(@java.util.List@.of(PathTracingConfig.builder()\n .path(\"/metrics/*\")\n .tracingConfig(TracingConfig.DISABLED)\n .build(), \n PathTracingConfig.builder()\n .path(\"/observe/metrics/*\")\n .tracingConfig(TracingConfig.DISABLED)\n .build(), \n PathTracingConfig.builder()\n .path(\"/health/*\")\n .tracingConfig(TracingConfig.DISABLED)\n .build(), \n PathTracingConfig.builder()\n .path(\"/observe/health/*\")\n .tracingConfig(TracingConfig.DISABLED)\n .build(), \n PathTracingConfig.builder()\n .path(\"/openapi/*\")\n .tracingConfig(TracingConfig.DISABLED)\n .build(), \n PathTracingConfig.builder()\n .path(\"/observe/openapi/*\")\n .tracingConfig(TracingConfig.DISABLED)\n .build()))", - "description": "Path specific configuration of tracing.\n\n @return configuration of tracing for specific paths", - "key": "paths", - "kind": "LIST", - "method": "io.helidon.webserver.observe.tracing.TracingObserverConfig.Builder#pathConfigs(java.util.List)", - "type": "io.helidon.webserver.observe.tracing.PathTracingConfig" - }, - { - "defaultValue": "900.0", - "description": "Weight of the feature registered with WebServer.\n Changing weight may cause tracing to be executed at a different time (such as after security, or even after\n all routes). Please understand feature weights before changing this order.\n\n @return weight of tracing feature", - "key": "weight", - "method": "io.helidon.webserver.observe.tracing.TracingObserverConfig.Builder#weight(double)", - "type": "java.lang.Double" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.observe.config", - "types": [ - { - "annotatedType": "io.helidon.webserver.observe.config.ConfigObserverConfig", - "type": "io.helidon.webserver.observe.config.ConfigObserver", - "producers": [ - "io.helidon.webserver.observe.config.ConfigObserverConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.config.ConfigObserverConfig#builder()", - "io.helidon.webserver.observe.config.ConfigObserver#create(io.helidon.webserver.observe.config.ConfigObserverConfig)" - ], - "provides": [ - "io.helidon.webserver.observe.spi.ObserveProvider" - ], - "options": [ - { - "defaultValue": "config", - "description": "", - "key": "endpoint", - "method": "io.helidon.webserver.observe.config.ConfigObserverConfig.Builder#endpoint(java.lang.String)" - }, - { - "defaultValue": ".*password, .*passphrase, .*secret", - "description": "Secret patterns (regular expressions) to exclude from output.\n Any pattern that matches a key will cause the output to be obfuscated and not contain the value.\n

\n Patterns always added:\n

    \n
  • `.*password`
  • \n
  • `.*passphrase`
  • \n
  • `.*secret`
  • \n
\n\n @return set of regular expression patterns for keys, where values should be excluded from output", - "key": "secrets", - "kind": "LIST", - "method": "io.helidon.webserver.observe.config.ConfigObserverConfig.Builder#secrets(java.util.Set)" - }, - { - "description": "Permit all access, even when not authorized.\n\n @return whether to permit access for anybody", - "key": "permit-all", - "method": "io.helidon.webserver.observe.config.ConfigObserverConfig.Builder#permitAll(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.observe.health", - "types": [ - { - "annotatedType": "io.helidon.webserver.observe.health.HealthObserverConfig", - "prefix": "health", - "type": "io.helidon.webserver.observe.health.HealthObserver", - "standalone": true, - "producers": [ - "io.helidon.webserver.observe.health.HealthObserverConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.health.HealthObserverConfig#builder()", - "io.helidon.webserver.observe.health.HealthObserver#create(io.helidon.webserver.observe.health.HealthObserverConfig)" - ], - "provides": [ - "io.helidon.webserver.observe.spi.ObserveProvider" - ], - "options": [ - { - "defaultValue": "health", - "description": "", - "key": "endpoint", - "method": "io.helidon.webserver.observe.health.HealthObserverConfig.Builder#endpoint(java.lang.String)" - }, - { - "defaultValue": "false", - "description": "Whether details should be printed.\n By default, health only returns a io.helidon.http.Status#NO_CONTENT_204 for success,\n io.helidon.http.Status#SERVICE_UNAVAILABLE_503 for health down,\n and io.helidon.http.Status#INTERNAL_SERVER_ERROR_500 in case of error with no entity.\n When details are enabled, health returns io.helidon.http.Status#OK_200 for success, same codes\n otherwise\n and a JSON entity with detailed information about each health check executed.\n\n @return set to `true` to enable details", - "key": "details", - "method": "io.helidon.webserver.observe.health.HealthObserverConfig.Builder#details(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Whether to use services discovered by java.util.ServiceLoader.\n By default, all io.helidon.health.spi.HealthCheckProvider based health checks are added.\n\n @return set to `false` to disable discovery", - "key": "use-system-services", - "method": "io.helidon.webserver.observe.health.HealthObserverConfig.Builder#useSystemServices(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.observe", - "types": [ - { - "annotatedType": "io.helidon.webserver.observe.ObserverConfigBase", - "type": "io.helidon.webserver.observe.ObserverConfigBase", - "producers": [ - "io.helidon.webserver.observe.ObserverConfigBase#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.ObserverConfigBase#builder()" - ], - "options": [ - { - "defaultValue": "true", - "description": "Whether this observer is enabled.\n\n @return `false` to disable observer", - "key": "enabled", - "method": "io.helidon.webserver.observe.ObserverConfigBase.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.observe.ObserveFeatureConfig", - "prefix": "observe", - "type": "io.helidon.webserver.observe.ObserveFeature", - "producers": [ - "io.helidon.webserver.observe.ObserveFeatureConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.ObserveFeatureConfig#builder()", - "io.helidon.webserver.observe.ObserveFeature#create(io.helidon.webserver.observe.ObserveFeatureConfig)" - ], - "provides": [ - "io.helidon.webserver.spi.ServerFeatureProvider" - ], - "options": [ - { - "defaultValue": "@io.helidon.cors.CrossOriginConfig@.create()", - "description": "Cors support inherited by each observe provider, unless explicitly configured.\n\n @return cors support to use", - "key": "cors", - "method": "io.helidon.webserver.observe.ObserveFeatureConfig.Builder#cors(io.helidon.cors.CrossOriginConfig)", - "type": "io.helidon.cors.CrossOriginConfig" - }, - { - "defaultValue": "/observe", - "description": "Root endpoint to use for observe providers. By default, all observe endpoint are under this root endpoint.\n

\n Example:\n
\n If root endpoint is `/observe` (the default), and default health endpoint is `health` (relative),\n health endpoint would be `/observe/health`.\n\n @return endpoint to use", - "key": "endpoint", - "method": "io.helidon.webserver.observe.ObserveFeatureConfig.Builder#endpoint(java.lang.String)" - }, - { - "description": "Sockets the observability endpoint should be exposed on. If not defined, defaults to the default socket\n ({@value io.helidon.webserver.WebServer#DEFAULT_SOCKET_NAME}.\n Each observer may have its own configuration of sockets that are relevant to it, this only controls the endpoints!\n\n @return list of sockets to register observe endpoint on", - "key": "sockets", - "kind": "LIST", - "method": "io.helidon.webserver.observe.ObserveFeatureConfig.Builder#sockets(java.util.List)" - }, - { - "defaultValue": "80.0", - "description": "Change the weight of this feature. This may change the order of registration of this feature.\n By default, observability weight is {@value ObserveFeature#WEIGHT} so it is registered after routing.\n\n @return weight to use", - "key": "weight", - "method": "io.helidon.webserver.observe.ObserveFeatureConfig.Builder#weight(double)", - "type": "java.lang.Double" - }, - { - "defaultValue": "true", - "description": "Whether the observe support is enabled.\n\n @return `false` to disable observe feature", - "key": "enabled", - "method": "io.helidon.webserver.observe.ObserveFeatureConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Observers to use with this observe features.\n Each observer type is registered only once, unless it uses a custom name (default name is the same as the type).\n\n @return list of observers to use in this feature", - "key": "observers", - "kind": "LIST", - "method": "io.helidon.webserver.observe.ObserveFeatureConfig.Builder#observers(java.util.List)", - "providerType": "io.helidon.webserver.observe.spi.ObserveProvider", - "type": "io.helidon.webserver.observe.spi.Observer", - "provider": true - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.observe.info", - "types": [ - { - "annotatedType": "io.helidon.webserver.observe.info.InfoObserverConfig", - "type": "io.helidon.webserver.observe.info.InfoObserver", - "producers": [ - "io.helidon.webserver.observe.info.InfoObserverConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.info.InfoObserverConfig#builder()", - "io.helidon.webserver.observe.info.InfoObserver#create(io.helidon.webserver.observe.info.InfoObserverConfig)" - ], - "provides": [ - "io.helidon.webserver.observe.spi.ObserveProvider" - ], - "options": [ - { - "defaultValue": "info", - "description": "", - "key": "endpoint", - "method": "io.helidon.webserver.observe.info.InfoObserverConfig.Builder#endpoint(java.lang.String)" - }, - { - "description": "Values to be exposed using this observability endpoint.\n\n @return value map", - "key": "values", - "kind": "MAP", - "method": "io.helidon.webserver.observe.info.InfoObserverConfig.Builder#values(java.util.Map)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.observe.log", - "types": [ - { - "annotatedType": "io.helidon.webserver.observe.log.LogStreamConfig", - "type": "io.helidon.webserver.observe.log.LogStreamConfig", - "producers": [ - "io.helidon.webserver.observe.log.LogStreamConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.log.LogStreamConfig#builder()" - ], - "options": [ - { - "defaultValue": "PT5S", - "description": "How long to wait before we send the idle message, to make sure we keep the stream alive.\n\n @return if no messages appear within this duration, and idle message will be sent\n @see #idleString()", - "key": "idle-message-timeout", - "method": "io.helidon.webserver.observe.log.LogStreamConfig.Builder#idleMessageTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "100", - "description": "Length of the in-memory queue that buffers log messages from loggers before sending them over the network.\n If the messages are produced faster than we can send them to client, excess messages are DISCARDED, and will not\n be sent.\n\n @return size of the in-memory queue for log messages", - "key": "queue-size", - "method": "io.helidon.webserver.observe.log.LogStreamConfig.Builder#queueSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "@io.helidon.http.HttpMediaTypes@.PLAINTEXT_UTF_8", - "description": "", - "key": "content-type", - "method": "io.helidon.webserver.observe.log.LogStreamConfig.Builder#contentType(io.helidon.http.HttpMediaType)", - "type": "io.helidon.http.HttpMediaType" - }, - { - "defaultValue": "%\n", - "description": "String sent when there are no log messages within the #idleMessageTimeout().\n\n @return string to write over the network when no log messages are received", - "key": "idle-string", - "method": "io.helidon.webserver.observe.log.LogStreamConfig.Builder#idleString(java.lang.String)" - }, - { - "defaultValue": "true", - "description": "Whether stream is enabled.\n\n @return whether to allow streaming of log statements", - "key": "enabled", - "method": "io.helidon.webserver.observe.log.LogStreamConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.observe.log.LogObserverConfig", - "type": "io.helidon.webserver.observe.log.LogObserver", - "producers": [ - "io.helidon.webserver.observe.log.LogObserverConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.observe.log.LogObserverConfig#builder()", - "io.helidon.webserver.observe.log.LogObserver#create(io.helidon.webserver.observe.log.LogObserverConfig)" - ], - "provides": [ - "io.helidon.webserver.observe.spi.ObserveProvider" - ], - "options": [ - { - "defaultValue": "log", - "description": "", - "key": "endpoint", - "method": "io.helidon.webserver.observe.log.LogObserverConfig.Builder#endpoint(java.lang.String)" - }, - { - "defaultValue": "@io.helidon.webserver.observe.log.LogStreamConfig@.create()", - "description": "Configuration of log stream.\n\n @return log stream configuration", - "key": "stream", - "method": "io.helidon.webserver.observe.log.LogObserverConfig.Builder#stream(io.helidon.webserver.observe.log.LogStreamConfig)", - "type": "io.helidon.webserver.observe.log.LogStreamConfig" - }, - { - "description": "Permit all access, even when not authorized.\n\n @return whether to permit access for anybody", - "key": "permit-all", - "method": "io.helidon.webserver.observe.log.LogObserverConfig.Builder#permitAll(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.http2", - "types": [ - { - "annotatedType": "io.helidon.webserver.http2.Http2Config", - "type": "io.helidon.webserver.http2.Http2Config", - "producers": [ - "io.helidon.webserver.http2.Http2Config#create(io.helidon.common.config.Config)", - "io.helidon.webserver.http2.Http2Config#builder()" - ], - "provides": [ - "io.helidon.webserver.spi.ProtocolConfig" - ], - "options": [ - { - "defaultValue": "8192", - "description": "Maximum number of concurrent streams that the server will allow.\n Defaults to `8192`. This limit is directional: it applies to the number of streams that the sender\n permits the receiver to create.\n It is recommended that this value be no smaller than 100 to not unnecessarily limit parallelism\n See RFC 9113 section 6.5.2 for details.\n\n @return maximal number of concurrent streams", - "key": "max-concurrent-streams", - "method": "io.helidon.webserver.http2.Http2Config.Builder#maxConcurrentStreams(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "PT10S", - "description": "Period for counting rapid resets(stream RST sent by client before any data have been sent by server).\n Default value is `PT10S`.\n\n @return duration\n @see CVE-2023-44487\n @see ISO_8601 Durations", - "key": "rapid-reset-check-period", - "method": "io.helidon.webserver.http2.Http2Config.Builder#rapidResetCheckPeriod(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "100", - "description": "Maximum number of rapid resets(stream RST sent by client before any data have been sent by server).\n When reached within #rapidResetCheckPeriod(), GOAWAY is sent to client and connection is closed.\n Default value is `100`.\n\n @return maximum number of rapid resets\n @see CVE-2023-44487", - "key": "max-rapid-resets", - "method": "io.helidon.webserver.http2.Http2Config.Builder#maxRapidResets(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "16384", - "description": "The size of the largest frame payload that the sender is willing to receive in bytes.\n Default value is `16384` and maximum value is 224-1 = 16777215 bytes.\n See RFC 9113 section 6.5.2 for details.\n\n @return maximal frame size", - "key": "max-frame-size", - "method": "io.helidon.webserver.http2.Http2Config.Builder#maxFrameSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "false", - "description": "Whether to send error message over HTTP to client.\n Defaults to `false`, as exception message may contain internal information that could be used as an\n attack vector. Use with care and in cases where both server and clients are under your full control (such as for\n testing).\n\n @return whether to send error messages over the network", - "key": "send-error-details", - "method": "io.helidon.webserver.http2.Http2Config.Builder#sendErrorDetails(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "8192", - "description": "The maximum field section size that the sender is prepared to accept in bytes.\n See RFC 9113 section 6.5.2 for details.\n Default is 8192.\n\n @return maximal header list size in bytes", - "key": "max-header-list-size", - "method": "io.helidon.webserver.http2.Http2Config.Builder#maxHeaderListSize(long)", - "type": "java.lang.Long" - }, - { - "description": "Requested URI discovery settings.\n\n @return settings for computing the requested URI", - "key": "requested-uri-discovery", - "method": "io.helidon.webserver.http2.Http2Config.Builder#requestedUriDiscovery(io.helidon.http.RequestedUriDiscoveryContext)", - "type": "io.helidon.http.RequestedUriDiscoveryContext" - }, - { - "defaultValue": "1048576", - "description": "This setting indicates the sender's maximum window size in bytes for stream-level flow control.\n Default and maximum value is 231-1 = 2147483647 bytes. This setting affects the window size\n of HTTP/2 connection.\n Any value greater than 2147483647 causes an error. Any value smaller than initial window size causes an error.\n See RFC 9113 section 6.9.1 for details.\n\n @return maximum window size in bytes", - "key": "initial-window-size", - "method": "io.helidon.webserver.http2.Http2Config.Builder#initialWindowSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "PT0.1S", - "description": "Outbound flow control blocking timeout configured as java.time.Duration\n or text in ISO-8601 format.\n Blocking timeout defines an interval to wait for the outbound window size changes(incoming window updates)\n before the next blocking iteration.\n Default value is `PT0.1S`.\n\n \n \n \n \n \n
ISO_8601 format examples:
PT0.1S100 milliseconds
PT0.5S500 milliseconds
PT2S2 seconds
\n\n @return duration\n @see ISO_8601 Durations", - "key": "flow-control-timeout", - "method": "io.helidon.webserver.http2.Http2Config.Builder#flowControlTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "true", - "description": "If set to false, any path is accepted (even containing illegal characters).\n\n @return whether to validate path", - "key": "validate-path", - "method": "io.helidon.webserver.http2.Http2Config.Builder#validatePath(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "10", - "description": "Maximum number of consecutive empty frames allowed on connection.\n\n @return max number of consecutive empty frames", - "key": "max-empty-frames", - "method": "io.helidon.webserver.http2.Http2Config.Builder#maxEmptyFrames(int)", - "type": "java.lang.Integer" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver", - "types": [ - { - "annotatedType": "io.helidon.webserver.WebServerConfig", - "prefix": "server", - "type": "io.helidon.webserver.WebServer", - "standalone": true, - "inherits": [ - "io.helidon.webserver.ListenerConfig" - ], - "producers": [ - "io.helidon.webserver.WebServerConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.WebServerConfig#builder()", - "io.helidon.webserver.WebServer#create(io.helidon.webserver.WebServerConfig)" - ], - "options": [ - { - "description": "Server features allow customization of the server, listeners, or routings.\n\n @return server features", - "key": "features", - "kind": "LIST", - "method": "io.helidon.webserver.WebServerConfig.Builder#features(java.util.List)", - "providerType": "io.helidon.webserver.spi.ServerFeatureProvider", - "type": "io.helidon.webserver.spi.ServerFeature", - "provider": true - }, - { - "description": "Socket configurations.\n Note that socket named {@value WebServer#DEFAULT_SOCKET_NAME} cannot be used,\n configure the values on the server directly.\n\n @return map of listener configurations, except for the default one", - "key": "sockets", - "kind": "MAP", - "method": "io.helidon.webserver.WebServerConfig.Builder#sockets(java.util.Map)", - "type": "io.helidon.webserver.ListenerConfig" - }, - { - "defaultValue": "true", - "description": "When true the webserver registers a shutdown hook with the JVM Runtime.\n

\n Defaults to true. Set this to false such that a shutdown hook is not registered.\n\n @return whether to register a shutdown hook", - "key": "shutdown-hook", - "method": "io.helidon.webserver.WebServerConfig.Builder#shutdownHook(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.ConnectionConfig", - "type": "io.helidon.webserver.ConnectionConfig", - "producers": [ - "io.helidon.webserver.ConnectionConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.ConnectionConfig#builder()" - ], - "options": [ - { - "defaultValue": "PT30S", - "description": "Read timeout.\n Default is {@value #DEFAULT_READ_TIMEOUT_DURATION}\n\n @return read timeout", - "key": "read-timeout", - "method": "io.helidon.webserver.ConnectionConfig.Builder#readTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "true", - "description": "Configure socket keep alive.\n Default is `true`.\n\n @return keep alive\n @see java.net.StandardSocketOptions#SO_KEEPALIVE", - "key": "keep-alive", - "method": "io.helidon.webserver.ConnectionConfig.Builder#keepAlive(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "PT10S", - "description": "Connect timeout.\n Default is {@value #DEFAULT_CONNECT_TIMEOUT_DURATION}.\n\n @return connect timeout", - "key": "connect-timeout", - "method": "io.helidon.webserver.ConnectionConfig.Builder#connectTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "32768", - "description": "Socket receive buffer size.\n Default is {@value #DEFAULT_SO_BUFFER_SIZE}.\n\n @return buffer size, in bytes\n @see java.net.StandardSocketOptions#SO_RCVBUF", - "key": "receive-buffer-size", - "method": "io.helidon.webserver.ConnectionConfig.Builder#receiveBufferSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Socket reuse address.\n Default is `true`.\n\n @return whether to reuse address\n @see java.net.StandardSocketOptions#SO_REUSEADDR", - "key": "reuse-address", - "method": "io.helidon.webserver.ConnectionConfig.Builder#reuseAddress(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Disable Nagle's algorithm by setting\n TCP_NODELAY to true. This can result in better performance on Mac or newer linux kernels for some\n payload types.\n Default is `false`.\n\n @return whether to use TCP_NODELAY, defaults to `false`\n @see java.net.StandardSocketOptions#TCP_NODELAY", - "key": "tcp-no-delay", - "method": "io.helidon.webserver.ConnectionConfig.Builder#tcpNoDelay(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "32768", - "description": "Socket send buffer size.\n Default is {@value #DEFAULT_SO_BUFFER_SIZE}.\n\n @return buffer size, in bytes\n @see java.net.StandardSocketOptions#SO_SNDBUF", - "key": "send-buffer-size", - "method": "io.helidon.webserver.ConnectionConfig.Builder#sendBufferSize(int)", - "type": "java.lang.Integer" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.http1.Http1Config", - "type": "io.helidon.webserver.http1.Http1Config", - "producers": [ - "io.helidon.webserver.http1.Http1Config#create(io.helidon.common.config.Config)", - "io.helidon.webserver.http1.Http1Config#builder()" - ], - "provides": [ - "io.helidon.webserver.spi.ProtocolConfig" - ], - "options": [ - { - "defaultValue": "false", - "description": "Whether to validate headers.\n If set to false, any value is accepted, otherwise validates headers + known headers\n are validated by format\n (content length is always validated as it is part of protocol processing (other headers may be validated if\n features use them)).\n

\n Defaults to `false` as user has control on the header creation.\n

\n\n @return whether to validate headers", - "key": "validate-response-headers", - "method": "io.helidon.webserver.http1.Http1Config.Builder#validateResponseHeaders(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "2048", - "description": "Maximal size of received HTTP prologue (GET /path HTTP/1.1).\n\n @return maximal size in bytes", - "key": "max-prologue-length", - "method": "io.helidon.webserver.http1.Http1Config.Builder#maxPrologueLength(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Logging of sent packets. Uses trace and debug levels on logger of\n Http1LoggingConnectionListener with suffix of `.send``.\n\n @return `true` if logging should be enabled for sent packets, `false` if no logging should be done", - "key": "send-log", - "method": "io.helidon.webserver.http1.Http1Config.Builder#sendLog(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Whether to validate headers.\n If set to false, any value is accepted, otherwise validates headers + known headers\n are validated by format\n (content length is always validated as it is part of protocol processing (other headers may be validated if\n features use them)).\n

\n Defaults to `true`.\n

\n\n @return whether to validate headers", - "key": "validate-request-headers", - "method": "io.helidon.webserver.http1.Http1Config.Builder#validateRequestHeaders(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Requested URI discovery settings.\n\n @return settings for computing the requested URI", - "key": "requested-uri-discovery", - "method": "io.helidon.webserver.http1.Http1Config.Builder#requestedUriDiscovery(io.helidon.http.RequestedUriDiscoveryContext)", - "type": "io.helidon.http.RequestedUriDiscoveryContext" - }, - { - "defaultValue": "16384", - "description": "Maximal size of received headers in bytes.\n\n @return maximal header size", - "key": "max-headers-size", - "method": "io.helidon.webserver.http1.Http1Config.Builder#maxHeadersSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "If set to false, any path is accepted (even containing illegal characters).\n\n @return whether to validate path", - "key": "validate-path", - "method": "io.helidon.webserver.http1.Http1Config.Builder#validatePath(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Logging of received packets. Uses trace and debug levels on logger of\n Http1LoggingConnectionListener with suffix of `.recv``.\n\n @return `true` if logging should be enabled for received packets, `false` if no logging should be done", - "key": "recv-log", - "method": "io.helidon.webserver.http1.Http1Config.Builder#receiveLog(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "When true WebServer answers to expect continue with 100 continue immediately,\n not waiting for user to actually request the data.\n\n @return if `true` answer with 100 continue immediately after expect continue", - "key": "continue-immediately", - "method": "io.helidon.webserver.http1.Http1Config.Builder#continueImmediately(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.ListenerConfig", - "type": "io.helidon.webserver.ListenerConfig", - "producers": [ - "io.helidon.webserver.ListenerConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.ListenerConfig#builder()" - ], - "options": [ - { - "defaultValue": "-1", - "description": "Limits the number of connections that can be opened at a single point in time.\n Defaults to `-1`, meaning \"unlimited\" - what the system allows.\n\n @return number of TCP connections that can be opened to this listener, regardless of protocol", - "key": "max-tcp-connections", - "method": "io.helidon.webserver.ListenerConfig.Builder#maxTcpConnections(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "0", - "description": "Number of buffers queued for write operations.\n\n @return maximal number of queued writes, defaults to 0", - "key": "write-queue-length", - "method": "io.helidon.webserver.ListenerConfig.Builder#writeQueueLength(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "512", - "description": "Initial buffer size in bytes of java.io.BufferedOutputStream created internally to\n write data to a socket connection. Default is `512`.\n\n @return initial buffer size used for writing", - "key": "write-buffer-size", - "method": "io.helidon.webserver.ListenerConfig.Builder#writeBufferSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "PT0.5S", - "description": "Grace period in ISO 8601 duration format to allow running tasks to complete before listener's shutdown.\n Default is `500` milliseconds.\n

Configuration file values example: `PT0.5S`, `PT2S`.\n\n @return grace period", - "key": "shutdown-grace-period", - "method": "io.helidon.webserver.ListenerConfig.Builder#shutdownGracePeriod(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "description": "Configure the listener specific io.helidon.http.encoding.ContentEncodingContext.\n This method discards all previously registered ContentEncodingContext.\n If no content encoding context is registered, content encoding context of the webserver would be used.\n\n @return content encoding context", - "key": "content-encoding", - "method": "io.helidon.webserver.ListenerConfig.Builder#contentEncoding(java.util.Optional)", - "type": "io.helidon.http.encoding.ContentEncodingContext" - }, - { - "defaultValue": "PT5M", - "description": "How long should we wait before closing a connection that has no traffic on it.\n Defaults to `PT5M` (5 minutes). Note that the timestamp is refreshed max. once per second, so this setting\n would be useless if configured for shorter periods of time (also not a very good support for connection keep alive,\n if the connections are killed so soon anyway).\n\n @return timeout of idle connections", - "key": "idle-connection-timeout", - "method": "io.helidon.webserver.ListenerConfig.Builder#idleConnectionTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "-1", - "description": "Limits the number of requests that can be executed at the same time (the number of active virtual threads of requests).\n Defaults to `-1`, meaning \"unlimited\" - what the system allows.\n Also make sure that this number is higher than the expected time it takes to handle a single request in your application,\n as otherwise you may stop in-progress requests.\n\n @return number of requests that can be processed on this listener, regardless of protocol", - "key": "max-concurrent-requests", - "method": "io.helidon.webserver.ListenerConfig.Builder#maxConcurrentRequests(int)", - "type": "java.lang.Integer" - }, - { - "description": "Requested URI discovery context.\n\n @return discovery context", - "key": "requested-uri-discovery", - "method": "io.helidon.webserver.ListenerConfig.Builder#requestedUriDiscoveryContext(java.util.Optional)", - "type": "io.helidon.http.RequestedUriDiscoveryContext" - }, - { - "description": "Configure the listener specific io.helidon.http.media.MediaContext.\n This method discards all previously registered MediaContext.\n If no media context is registered, media context of the webserver would be used.\n\n @return media context", - "key": "media-context", - "method": "io.helidon.webserver.ListenerConfig.Builder#mediaContext(java.util.Optional)", - "type": "io.helidon.http.media.MediaContext" - }, - { - "defaultValue": "131072", - "description": "If the entity is expected to be smaller that this number of bytes, it would be buffered in memory to optimize\n performance when writing it.\n If bigger, streaming will be used.\n

\n Note that for some entity types we cannot use streaming, as they are already fully in memory (String, byte[]), for such\n cases, this option is ignored.\n

\n Default is 128Kb.\n\n @return maximal number of bytes to buffer in memory for supported writers", - "key": "max-in-memory-entity", - "method": "io.helidon.webserver.ListenerConfig.Builder#maxInMemoryEntity(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "-1", - "description": "Maximal number of bytes an entity may have.\n If io.helidon.http.HeaderNames#CONTENT_LENGTH is used, this is checked immediately,\n if io.helidon.http.HeaderValues#TRANSFER_ENCODING_CHUNKED is used, we will fail when the\n number of bytes read would exceed the max payload size.\n Defaults to unlimited (`-1`).\n\n @return maximal number of bytes of entity", - "key": "max-payload-size", - "method": "io.helidon.webserver.ListenerConfig.Builder#maxPayloadSize(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "1024", - "description": "Accept backlog.\n\n @return backlog", - "key": "backlog", - "method": "io.helidon.webserver.ListenerConfig.Builder#backlog(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "0", - "description": "Port of the default socket.\n If configured to `0` (the default), server starts on a random port.\n\n @return port to listen on (for the default socket)", - "key": "port", - "method": "io.helidon.webserver.ListenerConfig.Builder#port(int)", - "type": "java.lang.Integer" - }, - { - "description": "Listener receive buffer size.\n\n @return buffer size in bytes", - "key": "receive-buffer-size", - "method": "io.helidon.webserver.ListenerConfig.Builder#receiveBufferSize(java.util.Optional)", - "type": "java.lang.Integer" - }, - { - "description": "Options for connections accepted by this listener.\n This is not used to setup server connection.\n\n @return socket options", - "key": "connection-options", - "method": "io.helidon.webserver.ListenerConfig.Builder#connectionOptions(io.helidon.common.socket.SocketOptions)", - "type": "io.helidon.common.socket.SocketOptions" - }, - { - "defaultValue": "0.0.0.0", - "description": "Host of the default socket. Defaults to all host addresses (`0.0.0.0`).\n\n @return host address to listen on (for the default socket)", - "key": "host", - "method": "io.helidon.webserver.ListenerConfig.Builder#host(java.lang.String)" - }, - { - "defaultValue": "@default", - "description": "Name of this socket. Defaults to `@default`.\n Must be defined if more than one socket is needed.\n\n @return name of the socket", - "key": "name", - "method": "io.helidon.webserver.ListenerConfig.Builder#name(java.lang.String)" - }, - { - "defaultValue": "PT2M", - "description": "How often should we check for #idleConnectionTimeout().\n Defaults to `PT2M` (2 minutes).\n\n @return period of checking for idle connections", - "key": "idle-connection-period", - "method": "io.helidon.webserver.ListenerConfig.Builder#idleConnectionPeriod(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "description": "Listener TLS configuration.\n\n @return tls of this configuration", - "key": "tls", - "method": "io.helidon.webserver.ListenerConfig.Builder#tls(java.util.Optional)", - "type": "io.helidon.common.tls.Tls" - }, - { - "description": "Configuration of protocols. This may be either protocol selectors, or protocol upgraders from HTTP/1.1.\n As the order is not important (providers are ordered by weight by default), we can use a configuration as an object,\n such as:\n

\n protocols:\n   providers:\n     http_1_1:\n       max-prologue-length: 8192\n     http_2:\n       max-frame-size: 4096\n     websocket:\n       ....\n 
\n\n @return all defined protocol configurations, loaded from service loader by default", - "key": "protocols", - "kind": "LIST", - "method": "io.helidon.webserver.ListenerConfig.Builder#protocols(java.util.List)", - "providerType": "io.helidon.webserver.spi.ProtocolConfigProvider", - "type": "io.helidon.webserver.spi.ProtocolConfig", - "provider": true - }, - { - "description": "Configuration of a connection (established from client against our server).\n\n @return connection configuration", - "key": "connection-config", - "method": "io.helidon.webserver.ListenerConfig.Builder#connectionConfig(java.util.Optional)", - "type": "io.helidon.webserver.ConnectionConfig" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.servicecommon", - "types": [ - { - "annotatedType": "io.helidon.webserver.servicecommon.HelidonFeatureSupport.Builder", - "type": "io.helidon.webserver.servicecommon.HelidonFeatureSupport.Builder", - "options": [ - { - "description": "Set the root context for the REST API of the service.", - "key": "web-context", - "method": "io.helidon.webserver.servicecommon.HelidonFeatureSupport.Builder#webContext(java.lang.String)" - }, - { - "description": "Set the CORS config from the specified `CrossOriginConfig` object.", - "key": "cross-origin-config", - "method": "io.helidon.webserver.servicecommon.HelidonFeatureSupport.Builder#crossOriginConfig(io.helidon.cors.CrossOriginConfig)", - "type": "io.helidon.cors.CrossOriginConfig" - } - ] - }, - { - "annotatedType": "io.helidon.webserver.servicecommon.RestServiceSettings.Builder", - "type": "io.helidon.webserver.servicecommon.RestServiceSettings.Builder", - "options": [ - { - "description": "Sets the web context to use for the service's endpoint.", - "key": "web-context", - "method": "io.helidon.webserver.servicecommon.RestServiceSettings.Builder#webContext(java.lang.String)", - "merge": true - }, - { - "description": "Sets the routing name to use for setting up the service's endpoint.", - "key": "routing", - "method": "io.helidon.webserver.servicecommon.RestServiceSettings.Builder#routing(java.lang.String)", - "merge": true - }, - { - "description": "Sets the cross-origin config builder for use in establishing CORS support for the service endpoints.", - "key": "cors", - "kind": "MAP", - "method": "io.helidon.webserver.servicecommon.RestServiceSettings.Builder#crossOriginConfig(io.helidon.cors.CrossOriginConfig.Builder)", - "type": "io.helidon.cors.CrossOriginConfig" - }, - { - "defaultValue": "true", - "description": "Is this service enabled or not.", - "key": "enabled", - "method": "io.helidon.webserver.servicecommon.RestServiceSettings.Builder#enabled(boolean)", - "type": "java.lang.Boolean", - "merge": true - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.webserver.cors", - "types": [ - { - "annotatedType": "io.helidon.webserver.cors.CorsConfig", - "prefix": "cors", - "type": "io.helidon.webserver.cors.CorsFeature", - "producers": [ - "io.helidon.webserver.cors.CorsConfig#create(io.helidon.common.config.Config)", - "io.helidon.webserver.cors.CorsConfig#builder()", - "io.helidon.webserver.cors.CorsFeature#create(io.helidon.webserver.cors.CorsConfig)" - ], - "provides": [ - "io.helidon.webserver.spi.ServerFeatureProvider" - ], - "options": [ - { - "description": "List of sockets to register this feature on. If empty, it would get registered on all sockets.\n\n @return socket names to register on, defaults to empty (all available sockets)", - "key": "sockets", - "kind": "LIST", - "method": "io.helidon.webserver.cors.CorsConfig.Builder#sockets(java.util.Set)" - }, - { - "defaultValue": "950.0", - "description": "Weight of the CORS feature. As it is used by other features, the default is quite high:\n {@value CorsFeature#WEIGHT}.\n\n @return weight of the feature", - "key": "weight", - "method": "io.helidon.webserver.cors.CorsConfig.Builder#weight(double)", - "type": "java.lang.Double" - }, - { - "description": "This feature can be disabled.\n\n @return whether the feature is enabled", - "key": "enabled", - "method": "io.helidon.webserver.cors.CorsConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean", - "required": true - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.common.configurable", - "types": [ - { - "annotatedType": "io.helidon.common.configurable.ResourceConfig", - "type": "io.helidon.common.configurable.Resource", - "producers": [ - "io.helidon.common.configurable.ResourceConfig#create(io.helidon.common.config.Config)", - "io.helidon.common.configurable.ResourceConfig#builder()", - "io.helidon.common.configurable.Resource#create(io.helidon.common.configurable.ResourceConfig)" - ], - "options": [ - { - "description": "Resource is located on filesystem.\n\n @return path of the resource", - "key": "path", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#path(java.util.Optional)", - "type": "java.nio.file.Path" - }, - { - "description": "Resource is located on classpath.\n\n @return classpath location of the resource", - "key": "resource-path", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#resourcePath(java.util.Optional)" - }, - { - "description": "Host of the proxy when using URI.\n\n @return proxy host", - "key": "proxy-host", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#proxyHost(java.util.Optional)" - }, - { - "description": "Resource is available on a java.net.URI.\n\n @return of the resource\n @see #proxy()\n @see #useProxy()", - "key": "uri", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#uri(java.util.Optional)", - "type": "java.net.URI" - }, - { - "defaultValue": "true", - "description": "Whether to use proxy. If set to `false`, proxy will not be used even if configured.\n When set to `true` (default), proxy will be used if configured.\n\n @return whether to use proxy if configured", - "key": "use-proxy", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#useProxy(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Plain content of the resource (text).\n\n @return plain content", - "key": "content-plain", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#contentPlain(java.util.Optional)" - }, - { - "defaultValue": "80", - "description": "Port of the proxy when using URI.\n\n @return proxy port", - "key": "proxy-port", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#proxyPort(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "", - "description": "Description of this resource when configured through plain text or binary.\n\n @return description", - "key": "description", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#description(java.lang.String)" - }, - { - "description": "Binary content of the resource (base64 encoded).\n\n @return binary content", - "key": "content", - "method": "io.helidon.common.configurable.ResourceConfig.Builder#content(java.util.Optional)" - } - ] - }, - { - "annotatedType": "io.helidon.common.configurable.ScheduledThreadPoolConfig", - "type": "io.helidon.common.configurable.ScheduledThreadPoolSupplier", - "producers": [ - "io.helidon.common.configurable.ScheduledThreadPoolConfig#create(io.helidon.common.config.Config)", - "io.helidon.common.configurable.ScheduledThreadPoolConfig#builder()", - "io.helidon.common.configurable.ScheduledThreadPoolSupplier#create(io.helidon.common.configurable.ScheduledThreadPoolConfig)" - ], - "options": [ - { - "defaultValue": "false", - "description": "Whether to prestart core threads in this thread pool executor.\n Defaults to {@value #DEFAULT_PRESTART}.\n\n @return whether to prestart the threads", - "key": "prestart", - "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#prestart(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "helidon-", - "description": "Name prefix for threads in this thread pool executor.\n Defaults to {@value #DEFAULT_THREAD_NAME_PREFIX}.\n\n @return prefix of a thread name", - "key": "thread-name-prefix", - "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#threadNamePrefix(java.lang.String)" - }, - { - "description": "When configured to `true`, an unbounded virtual executor service (project Loom) will be used.\n

\n If enabled, all other configuration options of this executor service are ignored!\n\n @return whether to use virtual threads or not, defaults to `false`", - "key": "virtual-threads", - "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#virtualThreads(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "16", - "description": "Core pool size of the thread pool executor.\n Defaults to {@value #DEFAULT_CORE_POOL_SIZE}.\n\n @return corePoolSize see java.util.concurrent.ThreadPoolExecutor#getCorePoolSize()", - "key": "core-pool-size", - "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#corePoolSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Is daemon of the thread pool executor.\n Defaults to {@value #DEFAULT_IS_DAEMON}.\n\n @return whether the threads are daemon threads", - "key": "is-daemon", - "method": "io.helidon.common.configurable.ScheduledThreadPoolConfig.Builder#daemon(boolean)", - "type": "java.lang.Boolean" - } - ] - }, - { - "annotatedType": "io.helidon.common.configurable.LruCacheConfig", - "type": "io.helidon.common.configurable.LruCache", - "producers": [ - "io.helidon.common.configurable.LruCacheConfig#create(io.helidon.common.config.Config)", - "io.helidon.common.configurable.LruCacheConfig#builder()", - "io.helidon.common.configurable.LruCache#create(io.helidon.common.configurable.LruCacheConfig)" - ], - "options": [ - { - "defaultValue": "10000", - "description": "Configure capacity of the cache. Defaults to {@value LruCache#DEFAULT_CAPACITY}.\n\n @return maximal number of records in the cache before the oldest one is removed", - "key": "capacity", - "method": "io.helidon.common.configurable.LruCacheConfig.Builder#capacity(int)", - "type": "java.lang.Integer" - } - ] - }, - { - "annotatedType": "io.helidon.common.configurable.ThreadPoolConfig", - "type": "io.helidon.common.configurable.ThreadPoolSupplier", - "producers": [ - "io.helidon.common.configurable.ThreadPoolConfig#create(io.helidon.common.config.Config)", - "io.helidon.common.configurable.ThreadPoolConfig#builder()", - "io.helidon.common.configurable.ThreadPoolSupplier#create(io.helidon.common.configurable.ThreadPoolConfig)" - ], - "options": [ - { - "defaultValue": "50", - "description": "Max pool size of the thread pool executor.\n Defaults to {@value #DEFAULT_MAX_POOL_SIZE}.\n\n @return maxPoolSize see java.util.concurrent.ThreadPoolExecutor#getMaximumPoolSize()", - "key": "max-pool-size", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#maxPoolSize(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "PT3M", - "description": "Keep alive of the thread pool executor.\n Defaults to {@value #DEFAULT_KEEP_ALIVE}.\n\n @return keep alive see java.util.concurrent.ThreadPoolExecutor#getKeepAliveTime(java.util.concurrent.TimeUnit)", - "key": "keep-alive", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#keepAlive(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "description": "Name prefix for threads in this thread pool executor.\n Defaults to {@value #DEFAULT_THREAD_NAME_PREFIX}.\n\n @return prefix of a thread name", - "key": "thread-name-prefix", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#threadNamePrefix(java.util.Optional)" - }, - { - "defaultValue": "true", - "description": "Whether to prestart core threads in this thread pool executor.\n Defaults to {@value #DEFAULT_PRESTART}.\n\n @return whether to prestart the threads", - "key": "should-prestart", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#shouldPrestart(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "When configured to `true`, an unbounded virtual executor service (project Loom) will be used.\n

\n If enabled, all other configuration options of this executor service are ignored!\n\n @return whether to use virtual threads or not, defaults to `false`", - "key": "virtual-threads", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#virtualThreads(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "10", - "description": "Core pool size of the thread pool executor.\n Defaults to {@value #DEFAULT_CORE_POOL_SIZE}.\n\n @return corePoolSize see java.util.concurrent.ThreadPoolExecutor#getCorePoolSize()", - "key": "core-pool-size", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#corePoolSize(int)", - "type": "java.lang.Integer" - }, - { - "description": "Name of this thread pool executor.\n\n @return the pool name", - "key": "name", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#name(java.util.Optional)" - }, - { - "defaultValue": "true", - "description": "Is daemon of the thread pool executor.\n Defaults to {@value #DEFAULT_IS_DAEMON}.\n\n @return whether the threads are daemon threads", - "key": "is-daemon", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#daemon(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "1000", - "description": "The queue size above which pool growth will be considered if the pool is not fixed size.\n Defaults to {@value #DEFAULT_GROWTH_THRESHOLD}.\n\n @return the growth threshold", - "key": "growth-threshold", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#growthThreshold(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "10000", - "description": "Queue capacity of the thread pool executor.\n Defaults to {@value #DEFAULT_QUEUE_CAPACITY}.\n\n @return capacity of the queue backing the executor", - "key": "queue-capacity", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#queueCapacity(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "0", - "description": "The percentage of task submissions that should result in adding threads, expressed as a value from 1 to 100. The\n rate applies only when all of the following are true:\n

    \n
  • the pool size is below the maximum, and
  • \n
  • there are no idle threads, and
  • \n
  • the number of tasks in the queue exceeds the `growthThreshold`
  • \n
\n For example, a rate of 20 means that while these conditions are met one thread will be added for every 5 submitted\n tasks.\n

\n Defaults to {@value #DEFAULT_GROWTH_RATE}\n\n @return the growth rate", - "key": "growth-rate", - "method": "io.helidon.common.configurable.ThreadPoolConfig.Builder#growthRate(int)", - "type": "java.lang.Integer" - } - ] - }, - { - "annotatedType": "io.helidon.common.configurable.AllowListConfig", - "type": "io.helidon.common.configurable.AllowList", - "producers": [ - "io.helidon.common.configurable.AllowListConfig#create(io.helidon.common.config.Config)", - "io.helidon.common.configurable.AllowListConfig#builder()", - "io.helidon.common.configurable.AllowList#create(io.helidon.common.configurable.AllowListConfig)" - ], - "options": [ - { - "defaultValue": "false", - "description": "Allows all strings to match (subject to \"deny\" conditions). An `allow.all` setting of `false` does\n not deny all strings but rather represents the absence of a universal match, meaning that other allow and deny settings\n determine the matching outcomes.\n\n @return whether to allow all strings to match (subject to \"deny\" conditions)", - "key": "allow.all", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowAll(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Patterns specifying strings to allow.\n\n @return patterns which allow matching", - "key": "allow.pattern", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowedPatterns(java.util.List)", - "type": "java.util.regex.Pattern" - }, - { - "description": "Suffixes specifying strings to deny.\n\n @return suffixes which deny matching", - "key": "deny.suffix", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#deniedSuffixes(java.util.List)" - }, - { - "description": "Prefixes specifying strings to allow.\n\n @return prefixes which allow matching", - "key": "allow.prefix", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowedPrefixes(java.util.List)" - }, - { - "description": "Exact strings to deny.\n\n @return exact strings to allow", - "key": "deny.exact", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#denied(java.util.List)" - }, - { - "description": "Patterns specifying strings to deny.\n\n @return patterns which deny matching", - "key": "deny.pattern", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#deniedPatterns(java.util.List)", - "type": "java.util.regex.Pattern" - }, - { - "description": "Exact strings to allow.\n\n @return exact strings to allow", - "key": "allow.exact", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowed(java.util.List)" - }, - { - "description": "Prefixes specifying strings to deny.\n\n @return prefixes which deny matching", - "key": "deny.prefix", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#deniedPrefixes(java.util.List)" - }, - { - "description": "Suffixes specifying strings to allow.\n\n @return suffixes which allow matching", - "key": "allow.suffix", - "kind": "LIST", - "method": "io.helidon.common.configurable.AllowListConfig.Builder#allowedSuffixes(java.util.List)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.common.pki", - "types": [ - { - "annotatedType": "io.helidon.common.pki.Keys", - "type": "io.helidon.common.pki.Keys", - "producers": [ - "io.helidon.common.pki.Keys#create(io.helidon.common.config.Config)", - "io.helidon.common.pki.Keys#builder()" - ], - "options": [ - { - "description": "Configure keys from pem file(s).\n Once the config object is built, this option will ALWAYS be empty. All keys from the keystore will be\n populated to #privateKey(), #publicKey(), #publicCert() etc.\n\n @return pem based definition", - "key": "pem", - "method": "io.helidon.common.pki.Keys.Builder#pem(java.util.Optional)", - "type": "io.helidon.common.pki.PemKeys" - }, - { - "description": "Configure keys from a keystore.\n Once the config object is built, this option will ALWAYS be empty. All keys from the keystore will be\n populated to #privateKey(), #publicKey(), #publicCert() etc.\n\n @return keystore configuration", - "key": "keystore", - "method": "io.helidon.common.pki.Keys.Builder#keystore(java.util.Optional)", - "type": "io.helidon.common.pki.KeystoreKeys" - } - ] - }, - { - "annotatedType": "io.helidon.common.pki.KeystoreKeys", - "type": "io.helidon.common.pki.KeystoreKeys", - "producers": [ - "io.helidon.common.pki.KeystoreKeys#create(io.helidon.common.config.Config)", - "io.helidon.common.pki.KeystoreKeys#builder()" - ], - "options": [ - { - "defaultValue": "false", - "description": "If you want to build a trust store, call this method to add all\n certificates present in the keystore to certificate list.\n\n @return whether this is a trust store", - "key": "trust-store", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#trustStore(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Alias of X.509 certificate of public key.\n Used to load both the certificate and public key.\n\n @return alias under which the certificate is stored in the keystore", - "key": "cert.alias", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#certAlias(java.util.Optional)" - }, - { - "description": "Alias of an X.509 chain.\n\n @return alias of certificate chain in the keystore", - "key": "cert-chain.alias", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#certChainAlias(java.util.Optional)" - }, - { - "description": "Keystore resource definition.\n\n @return keystore resource, from file path, classpath, URL etc.", - "key": "resource", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#keystore(io.helidon.common.configurable.Resource)", - "type": "io.helidon.common.configurable.Resource", - "required": true - }, - { - "description": "Pass-phrase of the keystore (supported with JKS and PKCS12 keystores).\n\n @return keystore password to use", - "key": "passphrase", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#passphrase(java.util.Optional)", - "type": "char[]" - }, - { - "description": "Alias of the private key in the keystore.\n\n @return alias of the key in the keystore", - "key": "key.alias", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#keyAlias(java.util.Optional)" - }, - { - "description": "Pass-phrase of the key in the keystore (used for private keys).\n This is (by default) the same as keystore passphrase - only configure\n if it differs from keystore passphrase.\n\n @return pass-phrase of the key", - "key": "key.passphrase", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#keyPassphrase(java.util.Optional)", - "type": "char[]" - }, - { - "defaultValue": "PKCS12", - "description": "Set type of keystore.\n Defaults to {@value #DEFAULT_KEYSTORE_TYPE},\n expected are other keystore types supported by java then can store keys under aliases.\n\n @return keystore type to load the key", - "key": "type", - "method": "io.helidon.common.pki.KeystoreKeys.Builder#type(java.lang.String)" - } - ] - }, - { - "annotatedType": "io.helidon.common.pki.PemKeys", - "type": "io.helidon.common.pki.PemKeys", - "producers": [ - "io.helidon.common.pki.PemKeys#create(io.helidon.common.config.Config)", - "io.helidon.common.pki.PemKeys#builder()" - ], - "options": [ - { - "description": "Load certificate chain from PEM resource.\n\n @return resource (e.g. classpath, file path, URL etc.)", - "key": "cert-chain.resource", - "method": "io.helidon.common.pki.PemKeys.Builder#certChain(java.util.Optional)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "description": "Read one or more certificates in PEM format from a resource definition. Used eg: in a trust store.\n\n @return key resource (file, classpath, URL etc.)", - "key": "certificates.resource", - "method": "io.helidon.common.pki.PemKeys.Builder#certificates(java.util.Optional)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "description": "Read a private key from PEM format from a resource definition.\n\n @return key resource (file, classpath, URL etc.)", - "key": "key.resource", - "method": "io.helidon.common.pki.PemKeys.Builder#key(java.util.Optional)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "description": "Read a public key from PEM format from a resource definition.\n\n @return public key resource (file, classpath, URL etc.)", - "key": "public-key.resource", - "method": "io.helidon.common.pki.PemKeys.Builder#publicKey(java.util.Optional)", - "type": "io.helidon.common.configurable.Resource" - }, - { - "description": "Passphrase for private key. If the key is encrypted (and in PEM PKCS#8 format), this passphrase will be used to\n decrypt it.\n\n @return passphrase used to encrypt the private key", - "key": "key.passphrase", - "method": "io.helidon.common.pki.PemKeys.Builder#keyPassphrase(java.util.Optional)", - "type": "char[]" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.common.tls", - "types": [ - { - "annotatedType": "io.helidon.common.tls.TlsConfig", - "type": "io.helidon.common.tls.Tls", - "producers": [ - "io.helidon.common.tls.TlsConfig#create(io.helidon.common.config.Config)", - "io.helidon.common.tls.TlsConfig#builder()", - "io.helidon.common.tls.Tls#create(io.helidon.common.tls.TlsConfig)" - ], - "options": [ - { - "defaultValue": "PT24H", - "description": "SSL session timeout.\n\n @return session timeout, defaults to {@value DEFAULT_SESSION_TIMEOUT}.", - "key": "session-timeout", - "method": "io.helidon.common.tls.TlsConfig.Builder#sessionTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "description": "The Tls manager. If one is not explicitly defined in the config then a default manager will be created.\n\n @return the tls manager of the tls instance\n @see ConfiguredTlsManager", - "key": "manager", - "method": "io.helidon.common.tls.TlsConfig.Builder#manager(io.helidon.common.tls.TlsManager)", - "providerType": "io.helidon.common.tls.spi.TlsManagerProvider", - "type": "io.helidon.common.tls.TlsManager", - "provider": true - }, - { - "description": "Provider of the key stores used internally to create a key and trust manager factories.\n\n @return keystore provider, if not defined, provider is not specified", - "key": "internal-keystore-provider", - "method": "io.helidon.common.tls.TlsConfig.Builder#internalKeystoreProvider(java.util.Optional)" - }, - { - "description": "Private key to use. For server side TLS, this is required.\n For client side TLS, this is optional (used when mutual TLS is enabled).\n\n @return private key to use", - "key": "private-key", - "method": "io.helidon.common.tls.TlsConfig.Builder#privateKey(java.util.Optional)", - "type": "java.security.PrivateKey" - }, - { - "defaultValue": "HTTPS", - "description": "Identification algorithm for SSL endpoints.\n\n @return configure endpoint identification algorithm, or set to `NONE`\n to disable endpoint identification (equivalent to hostname verification).\n Defaults to {@value Tls#ENDPOINT_IDENTIFICATION_HTTPS}", - "key": "endpoint-identification-algorithm", - "method": "io.helidon.common.tls.TlsConfig.Builder#endpointIdentificationAlgorithm(java.lang.String)" - }, - { - "description": "Algorithm of the key manager factory used when private key is defined.\n Defaults to javax.net.ssl.KeyManagerFactory#getDefaultAlgorithm().\n\n @return algorithm to use", - "key": "key-manager-factory-algorithm", - "method": "io.helidon.common.tls.TlsConfig.Builder#keyManagerFactoryAlgorithm(java.util.Optional)" - }, - { - "description": "Provider to use when creating a new secure random.\n When defined, #secureRandomAlgorithm() must be defined as well.\n\n @return provider to use, by default no provider is specified", - "key": "secure-random-provider", - "method": "io.helidon.common.tls.TlsConfig.Builder#secureRandomProvider(java.util.Optional)" - }, - { - "defaultValue": "20480", - "description": "SSL session cache size.\n\n @return session cache size, defaults to {@value DEFAULT_SESSION_CACHE_SIZE}.", - "key": "session-cache-size", - "method": "io.helidon.common.tls.TlsConfig.Builder#sessionCacheSize(int)", - "type": "java.lang.Integer" - }, - { - "description": "List of certificates that form the trust manager.\n\n @return certificates to be trusted", - "key": "trust", - "kind": "LIST", - "method": "io.helidon.common.tls.TlsConfig.Builder#trust(java.util.List)", - "type": "java.security.cert.X509Certificate" - }, - { - "defaultValue": "true", - "description": "Flag indicating whether Tls is enabled.\n\n @return enabled flag", - "key": "enabled", - "method": "io.helidon.common.tls.TlsConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Certificate revocation check configuration.\n\n @return certificate revocation configuration", - "key": "revocation", - "method": "io.helidon.common.tls.TlsConfig.Builder#revocation(java.util.Optional)", - "type": "io.helidon.common.tls.RevocationConfig" - }, - { - "description": "Use explicit provider to obtain an instance of javax.net.ssl.SSLContext.\n\n @return provider to use, defaults to none (only #protocol() is used by default)", - "key": "provider", - "method": "io.helidon.common.tls.TlsConfig.Builder#provider(java.util.Optional)" - }, - { - "description": "Enabled cipher suites for TLS communication.\n\n @return cipher suits to enable, by default (or if list is empty), all available cipher suites\n are enabled", - "key": "cipher-suite", - "kind": "LIST", - "method": "io.helidon.common.tls.TlsConfig.Builder#enabledCipherSuites(java.util.List)" - }, - { - "defaultValue": "NONE", - "description": "Configure requirement for mutual TLS.\n\n @return what type of mutual TLS to use, defaults to TlsClientAuth#NONE", - "key": "client-auth", - "method": "io.helidon.common.tls.TlsConfig.Builder#clientAuth(io.helidon.common.tls.TlsClientAuth)", - "type": "io.helidon.common.tls.TlsClientAuth", - "allowedValues": [ - { - "description": "Mutual TLS is required.\n Server MUST present a certificate trusted by the client, client MUST present a certificate trusted by the server.\n This implies private key and trust configuration for both server and client.", - "value": "REQUIRED" - }, - { - "description": "Mutual TLS is optional.\n Server MUST present a certificate trusted by the client, client MAY present a certificate trusted by the server.\n This implies private key configuration at least for server, trust configuration for at least client.", - "value": "OPTIONAL" - }, - { - "description": "Mutual TLS is disabled.\n Server MUST present a certificate trusted by the client, client does not present a certificate.\n This implies private key configuration for server, trust configuration for client.", - "value": "NONE" - } - ] - }, - { - "description": "Trust manager factory algorithm.\n\n @return algorithm to use", - "key": "trust-manager-factory-algorithm", - "method": "io.helidon.common.tls.TlsConfig.Builder#trustManagerFactoryAlgorithm(java.util.Optional)" - }, - { - "description": "Type of the key stores used internally to create a key and trust manager factories.\n\n @return keystore type, defaults to java.security.KeyStore#getDefaultType()", - "key": "internal-keystore-type", - "method": "io.helidon.common.tls.TlsConfig.Builder#internalKeystoreType(java.util.Optional)" - }, - { - "defaultValue": "false", - "description": "Trust any certificate provided by the other side of communication.\n

\n This is a dangerous setting: if set to `true`, any certificate will be accepted, throwing away\n most of the security advantages of TLS. NEVER do this in production.\n\n @return whether to trust all certificates, do not use in production", - "key": "trust-all", - "method": "io.helidon.common.tls.TlsConfig.Builder#trustAll(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "TLS", - "description": "Configure the protocol used to obtain an instance of javax.net.ssl.SSLContext.\n\n @return protocol to use, defaults to {@value DEFAULT_PROTOCOL}", - "key": "protocol", - "method": "io.helidon.common.tls.TlsConfig.Builder#protocol(java.lang.String)" - }, - { - "description": "Enabled protocols for TLS communication.\n Example of valid values for `TLS` protocol: `TLSv1.3`, `TLSv1.2`\n\n @return protocols to enable, by default (or if list is empty), all available protocols are enabled", - "key": "protocols", - "kind": "LIST", - "method": "io.helidon.common.tls.TlsConfig.Builder#enabledProtocols(java.util.List)" - }, - { - "description": "Algorithm to use when creating a new secure random.\n\n @return algorithm to use, by default uses java.security.SecureRandom constructor", - "key": "secure-random-algorithm", - "method": "io.helidon.common.tls.TlsConfig.Builder#secureRandomAlgorithm(java.util.Optional)" - } - ] - }, - { - "annotatedType": "io.helidon.common.tls.RevocationConfig", - "type": "io.helidon.common.tls.RevocationConfig", - "producers": [ - "io.helidon.common.tls.RevocationConfig#create(io.helidon.common.config.Config)", - "io.helidon.common.tls.RevocationConfig#builder()" - ], - "options": [ - { - "defaultValue": "false", - "description": "Allow revocation check to succeed if the revocation status cannot be\n determined for one of the following reasons:\n

    \n
  • The CRL or OCSP response cannot be obtained because of a\n network error.\n
  • The OCSP responder returns one of the following errors\n specified in section 2.3 of RFC 2560: internalError or tryLater.\n
\n\n @return whether soft fail is enabled", - "key": "soft-fail-enabled", - "method": "io.helidon.common.tls.RevocationConfig.Builder#softFailEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "The URI that identifies the location of the OCSP responder. This\n overrides the `ocsp.responderURL` security property and any\n responder specified in a certificate's Authority Information Access\n Extension, as defined in RFC 5280.\n\n @return OCSP responder URI", - "key": "ocsp-responder-uri", - "method": "io.helidon.common.tls.RevocationConfig.Builder#ocspResponderUri(java.util.Optional)", - "type": "java.net.URI" - }, - { - "defaultValue": "false", - "description": "Prefer CRL over OCSP.\n Default value is `false`. OCSP is preferred over the CRL by default.\n\n @return whether to prefer CRL over OCSP", - "key": "prefer-crl-over-ocsp", - "method": "io.helidon.common.tls.RevocationConfig.Builder#preferCrlOverOcsp(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Only check the revocation status of end-entity certificates.\n Default value is `false`.\n\n @return whether to check only end-entity certificates", - "key": "check-only-end-entity", - "method": "io.helidon.common.tls.RevocationConfig.Builder#checkOnlyEndEntity(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "description": "Enable fallback to the less preferred checking option.\n
\n If the primary method for revocation checking fails to verify the revocation status of a certificate\n (such as using a CRL or OCSP), the checker will attempt alternative methods. This option ensures\n whether revocation checking is performed strictly according to the specified method, or should fallback\n to the one less preferred. OCSP is preferred over the CRL by default.\n\n @return whether to allow fallback to the less preferred checking option", - "key": "fallback-enabled", - "method": "io.helidon.common.tls.RevocationConfig.Builder#fallbackEnabled(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "false", - "description": "Flag indicating whether this revocation config is enabled.\n\n @return enabled flag", - "key": "enabled", - "method": "io.helidon.common.tls.RevocationConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.common.socket", - "types": [ - { - "annotatedType": "io.helidon.common.socket.SocketOptions", - "type": "io.helidon.common.socket.SocketOptions", - "producers": [ - "io.helidon.common.socket.SocketOptions#create(io.helidon.common.config.Config)", - "io.helidon.common.socket.SocketOptions#builder()" - ], - "options": [ - { - "defaultValue": "PT30S", - "description": "Socket read timeout. Default is 30 seconds.\n\n @return read timeout duration", - "key": "read-timeout", - "method": "io.helidon.common.socket.SocketOptions.Builder#readTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "description": "Socket send buffer size.\n\n @return buffer size, in bytes\n @see java.net.StandardSocketOptions#SO_SNDBUF", - "key": "socket-send-buffer-size", - "method": "io.helidon.common.socket.SocketOptions.Builder#socketSendBufferSize(java.util.Optional)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Socket reuse address.\n Default is `true`.\n\n @return whether to reuse address\n @see java.net.StandardSocketOptions#SO_REUSEADDR", - "key": "socket-reuse-address", - "method": "io.helidon.common.socket.SocketOptions.Builder#socketReuseAddress(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "PT10S", - "description": "Socket connect timeout. Default is 10 seconds.\n\n @return connect timeout duration", - "key": "connect-timeout", - "method": "io.helidon.common.socket.SocketOptions.Builder#connectTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "false", - "description": "This option may improve performance on some systems.\n Default is `false`.\n\n @return whether to use TCP_NODELAY, defaults to `false`\n @see java.net.StandardSocketOptions#TCP_NODELAY", - "key": "tcp-no-delay", - "method": "io.helidon.common.socket.SocketOptions.Builder#tcpNoDelay(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Socket receive buffer size.\n\n @return buffer size, in bytes\n @see java.net.StandardSocketOptions#SO_RCVBUF", - "key": "socket-receive-buffer-size", - "method": "io.helidon.common.socket.SocketOptions.Builder#socketReceiveBufferSize(java.util.Optional)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "true", - "description": "Configure socket keep alive.\n Default is `true`.\n\n @return keep alive\n @see java.net.StandardSocketOptions#SO_KEEPALIVE", - "key": "socket-keep-alive", - "method": "io.helidon.common.socket.SocketOptions.Builder#socketKeepAlive(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.http.encoding", - "types": [ - { - "annotatedType": "io.helidon.http.encoding.ContentEncodingContextConfig", - "type": "io.helidon.http.encoding.ContentEncodingContext", - "producers": [ - "io.helidon.http.encoding.ContentEncodingContextConfig#create(io.helidon.common.config.Config)", - "io.helidon.http.encoding.ContentEncodingContextConfig#builder()", - "io.helidon.http.encoding.ContentEncodingContext#create(io.helidon.http.encoding.ContentEncodingContextConfig)" - ], - "options": [ - { - "description": "List of content encodings that should be used.\n Encodings configured here have priority over encodings discovered through service loader.\n\n @return list of content encodings to be used (such as `gzip,deflate`)", - "key": "content-encodings", - "kind": "LIST", - "method": "io.helidon.http.encoding.ContentEncodingContextConfig.Builder#contentEncodings(java.util.List)", - "providerType": "io.helidon.http.encoding.spi.ContentEncodingProvider", - "type": "io.helidon.http.encoding.ContentEncoding", - "provider": true - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.http", - "types": [ - { - "annotatedType": "io.helidon.http.RequestedUriDiscoveryContext.Builder", - "type": "io.helidon.http.RequestedUriDiscoveryContext", - "producers": [ - "io.helidon.http.RequestedUriDiscoveryContext.Builder#build()", - "io.helidon.http.RequestedUriDiscoveryContext#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "description": "Sets the discovery types for requested URI discovery for requests arriving on the socket.", - "key": "types", - "kind": "LIST", - "method": "io.helidon.http.RequestedUriDiscoveryContext.Builder#types(java.util.List)", - "type": "io.helidon.http.RequestedUriDiscoveryContext.RequestedUriDiscoveryType", - "allowedValues": [ - { - "description": "The `io.helidon.http.Header#FORWARDED` header is used to discover the original requested URI.", - "value": "FORWARDED" - }, - { - "description": "The\n `io.helidon.http.Header#X_FORWARDED_PROTO`,\n `io.helidon.http.Header#X_FORWARDED_HOST`,\n `io.helidon.http.Header#X_FORWARDED_PORT`,\n `io.helidon.http.Header#X_FORWARDED_PREFIX`\n headers are used to discover the original requested URI.", - "value": "X_FORWARDED" - }, - { - "description": "This is the default, only the `io.helidon.http.Header#HOST` header is used to discover\n requested URI.", - "value": "HOST" - } - ] - }, - { - "description": "Sets the trusted proxies for requested URI discovery for requests arriving on the socket.", - "key": "trusted-proxies", - "method": "io.helidon.http.RequestedUriDiscoveryContext.Builder#trustedProxies(io.helidon.common.configurable.AllowList)", - "type": "io.helidon.common.configurable.AllowList" - }, - { - "defaultValue": "true if 'types' or 'trusted-proxies' is set; false otherwise", - "description": "Sets whether requested URI discovery is enabled for requestes arriving on the socket.", - "key": "enabled", - "method": "io.helidon.http.RequestedUriDiscoveryContext.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.http.media", - "types": [ - { - "annotatedType": "io.helidon.http.media.MediaContextConfig", - "type": "io.helidon.http.media.MediaContext", - "producers": [ - "io.helidon.http.media.MediaContextConfig#create(io.helidon.common.config.Config)", - "io.helidon.http.media.MediaContextConfig#builder()", - "io.helidon.http.media.MediaContext#create(io.helidon.http.media.MediaContextConfig)" - ], - "options": [ - { - "defaultValue": "true", - "description": "Should we register defaults of Helidon, such as String media support.\n\n @return whether to register default media supports", - "key": "register-defaults", - "method": "io.helidon.http.media.MediaContextConfig.Builder#registerDefaults(boolean)", - "type": "java.lang.Boolean" - }, - { - "description": "Media supports to use.\n This instance has priority over provider(s) discovered by service loader.\n The providers are used in order of calling this method, where the first support added is the\n first one to be queried for readers and writers.\n\n @return media supports", - "key": "media-supports", - "kind": "LIST", - "method": "io.helidon.http.media.MediaContextConfig.Builder#mediaSupports(java.util.List)", - "providerType": "io.helidon.http.media.spi.MediaSupportProvider", - "type": "io.helidon.http.media.MediaSupport", - "provider": true - }, - { - "description": "Existing context to be used as a fallback for this context.\n\n @return media context to use if supports configured on this request cannot provide a good result", - "key": "fallback", - "method": "io.helidon.http.media.MediaContextConfig.Builder#fallback(java.util.Optional)", - "type": "io.helidon.http.media.MediaContext" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.faulttolerance", - "types": [ - { - "annotatedType": "io.helidon.faulttolerance.CircuitBreakerConfig", - "prefix": "fault-tolerance.circuit-breakers", - "type": "io.helidon.faulttolerance.CircuitBreaker", - "standalone": true, - "producers": [ - "io.helidon.faulttolerance.CircuitBreakerConfig#create(io.helidon.common.config.Config)", - "io.helidon.faulttolerance.CircuitBreakerConfig#builder()", - "io.helidon.faulttolerance.CircuitBreaker#create(io.helidon.faulttolerance.CircuitBreakerConfig)" - ], - "options": [ - { - "defaultValue": "PT5S", - "description": "How long to wait before transitioning from open to half-open state.\n\n @return delay", - "key": "delay", - "method": "io.helidon.faulttolerance.CircuitBreakerConfig.Builder#delay(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "10", - "description": "Rolling window size used to calculate ratio of failed requests.\n Default is {@value #DEFAULT_VOLUME}.\n\n @return how big a window is used to calculate error errorRatio\n @see #errorRatio()", - "key": "volume", - "method": "io.helidon.faulttolerance.CircuitBreakerConfig.Builder#volume(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "60", - "description": "How many failures out of 100 will trigger the circuit to open.\n This is adapted to the #volume() used to handle the window of requests.\n

If errorRatio is 40, and volume is 10, 4 failed requests will open the circuit.\n Default is {@value #DEFAULT_ERROR_RATIO}.\n\n @return percent of failure that trigger the circuit to open\n @see #volume()", - "key": "error-ratio", - "method": "io.helidon.faulttolerance.CircuitBreakerConfig.Builder#errorRatio(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "1", - "description": "How many successful calls will close a half-open circuit.\n Nevertheless, the first failed call will open the circuit again.\n Default is {@value #DEFAULT_SUCCESS_THRESHOLD}.\n\n @return number of calls", - "key": "success-threshold", - "method": "io.helidon.faulttolerance.CircuitBreakerConfig.Builder#successThreshold(int)", - "type": "java.lang.Integer" - } - ] - }, - { - "annotatedType": "io.helidon.faulttolerance.AsyncConfig", - "type": "io.helidon.faulttolerance.Async", - "producers": [ - "io.helidon.faulttolerance.AsyncConfig#create(io.helidon.common.config.Config)", - "io.helidon.faulttolerance.AsyncConfig#builder()", - "io.helidon.faulttolerance.Async#create(io.helidon.faulttolerance.AsyncConfig)" - ], - "options": [ - { - "description": "Name of an executor service. This is only honored when service registry is used.\n\n @return name fo the java.util.concurrent.ExecutorService to lookup\n @see #executor()", - "key": "executor-name", - "method": "io.helidon.faulttolerance.AsyncConfig.Builder#executorName(java.util.Optional)" - } - ] - }, - { - "annotatedType": "io.helidon.faulttolerance.BulkheadConfig", - "prefix": "fault-tolerance.bulkheads", - "type": "io.helidon.faulttolerance.Bulkhead", - "standalone": true, - "producers": [ - "io.helidon.faulttolerance.BulkheadConfig#create(io.helidon.common.config.Config)", - "io.helidon.faulttolerance.BulkheadConfig#builder()", - "io.helidon.faulttolerance.Bulkhead#create(io.helidon.faulttolerance.BulkheadConfig)" - ], - "options": [ - { - "defaultValue": "10", - "description": "Maximal number of parallel requests going through this bulkhead.\n When the limit is reached, additional requests are enqueued.\n\n @return maximal number of parallel calls, defaults is {@value DEFAULT_LIMIT}", - "key": "limit", - "method": "io.helidon.faulttolerance.BulkheadConfig.Builder#limit(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "10", - "description": "Maximal number of enqueued requests waiting for processing.\n When the limit is reached, additional attempts to invoke\n a request will receive a BulkheadException.\n\n @return length of the queue", - "key": "queue-length", - "method": "io.helidon.faulttolerance.BulkheadConfig.Builder#queueLength(int)", - "type": "java.lang.Integer" - } - ] - }, - { - "annotatedType": "io.helidon.faulttolerance.TimeoutConfig", - "prefix": "fault-tolerance.timeouts", - "type": "io.helidon.faulttolerance.Timeout", - "standalone": true, - "producers": [ - "io.helidon.faulttolerance.TimeoutConfig#create(io.helidon.common.config.Config)", - "io.helidon.faulttolerance.TimeoutConfig#builder()", - "io.helidon.faulttolerance.Timeout#create(io.helidon.faulttolerance.TimeoutConfig)" - ], - "options": [ - { - "defaultValue": "false", - "description": "Flag to indicate that code must be executed in current thread instead\n of in an executor's thread. This flag is `false` by default.\n\n @return whether to execute on current thread (`true`), or in an executor service (`false`})", - "key": "current-thread", - "method": "io.helidon.faulttolerance.TimeoutConfig.Builder#currentThread(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "PT10S", - "description": "Duration to wait before timing out.\n Defaults to `10 seconds`.\n\n @return timeout", - "key": "timeout", - "method": "io.helidon.faulttolerance.TimeoutConfig.Builder#timeout(java.time.Duration)", - "type": "java.time.Duration" - } - ] - }, - { - "annotatedType": "io.helidon.faulttolerance.RetryConfig", - "prefix": "fault-tolerance.retries", - "type": "io.helidon.faulttolerance.Retry", - "standalone": true, - "producers": [ - "io.helidon.faulttolerance.RetryConfig#create(io.helidon.common.config.Config)", - "io.helidon.faulttolerance.RetryConfig#builder()", - "io.helidon.faulttolerance.Retry#create(io.helidon.faulttolerance.RetryConfig)" - ], - "options": [ - { - "defaultValue": "PT1S", - "description": "Overall timeout of all retries combined.\n\n @return overall timeout", - "key": "overall-timeout", - "method": "io.helidon.faulttolerance.RetryConfig.Builder#overallTimeout(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "PT0.2S", - "description": "Base delay between try and retry.\n Defaults to `200 ms`.\n\n @return delay between retries (combines with retry policy)", - "key": "delay", - "method": "io.helidon.faulttolerance.RetryConfig.Builder#delay(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "3", - "description": "Number of calls (first try + retries).\n\n @return number of desired calls, must be 1 (means no retries) or higher.", - "key": "calls", - "method": "io.helidon.faulttolerance.RetryConfig.Builder#calls(int)", - "type": "java.lang.Integer" - }, - { - "defaultValue": "PT-1S", - "description": "Jitter for Retry.JitterRetryPolicy. If unspecified (value of `-1`),\n delaying retry policy is used. If both this value, and #delayFactor() are specified, delaying retry policy\n would be used.\n\n @return jitter", - "key": "jitter", - "method": "io.helidon.faulttolerance.RetryConfig.Builder#jitter(java.time.Duration)", - "type": "java.time.Duration" - }, - { - "defaultValue": "-1.0", - "description": "Delay retry policy factor. If unspecified (value of `-1`), Jitter retry policy would be used, unless\n jitter is also unspecified.\n

\n Default when Retry.DelayingRetryPolicy is used is `2`.\n\n @return delay factor for delaying retry policy", - "key": "delay-factor", - "method": "io.helidon.faulttolerance.RetryConfig.Builder#delayFactor(double)", - "type": "java.lang.Double" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.cors", - "types": [ - { - "annotatedType": "io.helidon.cors.CrossOriginConfig.Builder", - "type": "io.helidon.cors.CrossOriginConfig", - "producers": [ - "io.helidon.cors.CrossOriginConfig.Builder#build()", - "io.helidon.cors.CrossOriginConfig#create(io.helidon.common.config.Config)" - ], - "options": [ - { - "defaultValue": "{+}", - "description": "Updates the path prefix for this cross-origin config.", - "key": "path-pattern", - "method": "io.helidon.cors.CrossOriginConfig.Builder#pathPattern(java.lang.String)" - }, - { - "defaultValue": "*", - "description": "Sets the allow headers.", - "key": "allow-headers", - "kind": "LIST", - "method": "io.helidon.cors.CrossOriginConfig.Builder#allowHeaders(java.lang.String[])" - }, - { - "defaultValue": "3600", - "description": "Sets the maximum age.", - "key": "max-age-seconds", - "method": "io.helidon.cors.CrossOriginConfig.Builder#maxAgeSeconds(long)", - "type": "java.lang.Long" - }, - { - "defaultValue": "false", - "description": "Sets the allow credentials flag.", - "key": "allow-credentials", - "method": "io.helidon.cors.CrossOriginConfig.Builder#allowCredentials(boolean)", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "*", - "description": "Sets the allowOrigins.", - "key": "allow-origins", - "kind": "LIST", - "method": "io.helidon.cors.CrossOriginConfig.Builder#allowOrigins(java.lang.String[])" - }, - { - "description": "Sets the expose headers.", - "key": "expose-headers", - "kind": "LIST", - "method": "io.helidon.cors.CrossOriginConfig.Builder#exposeHeaders(java.lang.String[])" - }, - { - "defaultValue": "*", - "description": "Sets the allow methods.", - "key": "allow-methods", - "kind": "LIST", - "method": "io.helidon.cors.CrossOriginConfig.Builder#allowMethods(java.lang.String[])" - }, - { - "defaultValue": "true", - "description": "Sets whether this config should be enabled or not.", - "key": "enabled", - "method": "io.helidon.cors.CrossOriginConfig.Builder#enabled(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.microprofile.server", - "types": [ - { - "annotatedType": "io.helidon.microprofile.server.Server.Builder", - "description": "Configuration of Helidon Microprofile Server", - "prefix": "server", - "type": "io.helidon.microprofile.server.Server", - "standalone": true, - "producers": [ - "io.helidon.microprofile.server.Server.Builder#build()" - ], - "options": [ - { - "description": "Configure listen port.", - "key": "port", - "method": "io.helidon.microprofile.server.Server.Builder#port(int)", - "type": "java.lang.Integer" - }, - { - "description": "Configure listen host.", - "key": "host", - "method": "io.helidon.microprofile.server.Server.Builder#host(java.lang.String)" - } - ] - } - ] - } -] -[ - { - "module": "io.helidon.microprofile.openapi", - "types": [ - { - "annotatedType": "io.helidon.microprofile.openapi.MpOpenApiManagerConfig", - "type": "io.helidon.microprofile.openapi.MpOpenApiManagerConfig", - "producers": [ - "io.helidon.microprofile.openapi.MpOpenApiManagerConfig#create(io.helidon.common.config.Config)", - "io.helidon.microprofile.openapi.MpOpenApiManagerConfig#builder()" - ], - "options": [ - { - "description": "If `true` and the `jakarta.ws.rs.core.Application` class returns a non-empty set, endpoints defined by\n other resources are not included in the OpenAPI document.\n\n @return `true` if enabled, `false` otherwise", - "key": "mp.openapi.extensions.helidon.use-jaxrs-semantics", - "method": "io.helidon.microprofile.openapi.MpOpenApiManagerConfig.Builder#useJaxRsSemantics(boolean)", - "type": "java.lang.Boolean" - } - ] - } - ] - } -] diff --git a/all/pom.xml b/all/pom.xml index 58d2d35cba8..3ae9b408680 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom helidon-all @@ -133,6 +133,16 @@ io.helidon.config helidon-config-metadata-processor + + io.helidon.config.metadata + helidon-config-metadata-codegen + + io.helidon.security helidon-security @@ -281,6 +291,22 @@ io.helidon.microprofile.service-common helidon-microprofile-service-common + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-core + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-server + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-client + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-tracing + io.helidon.metrics helidon-metrics @@ -437,6 +463,10 @@ io.helidon.common.features helidon-common-features + + io.helidon.common.concurrency + helidon-common-concurrency-limits + io.helidon.dbclient helidon-dbclient @@ -509,6 +539,14 @@ io.helidon.tracing helidon-tracing-jersey + + io.helidon.grpc + helidon-grpc-api + + + io.helidon.grpc + helidon-grpc-core + io.helidon.microprofile.tracing helidon-microprofile-tracing @@ -521,6 +559,10 @@ io.helidon.microprofile.rest-client helidon-microprofile-rest-client + + io.helidon.microprofile.rest-client-metrics + helidon-microprofile-rest-client-metrics + io.helidon.microprofile.reactive-streams helidon-microprofile-reactive-streams @@ -565,6 +607,10 @@ io.helidon.integrations.db helidon-integrations-db-mysql + + io.helidon.integrations.db + helidon-integrations-db-pgsql + io.helidon.integrations.cdi helidon-integrations-cdi-configurable @@ -641,6 +687,18 @@ io.helidon.integrations.oci helidon-integrations-oci + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-instance + + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-resource + + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-oke-workload + io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-cdi @@ -946,6 +1004,10 @@ io.helidon.webserver helidon-webserver-service-common + + io.helidon.webserver + helidon-webserver-concurrency-limits + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 @@ -1058,6 +1120,10 @@ io.helidon.inject.configdriven helidon-inject-configdriven-processor + + io.helidon.service + helidon-service-metadata + io.helidon.service helidon-service-registry @@ -1066,6 +1132,22 @@ io.helidon.service helidon-service-codegen + + io.helidon.service.inject + helidon-service-inject-codegen + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + io.helidon.metadata + helidon-metadata-hson + io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-processor @@ -1076,16 +1158,4 @@ - - - - - org.antlr - antlr4-runtime - 4.10.1 - - - diff --git a/applications/mp/pom.xml b/applications/mp/pom.xml index cece884cb9c..4afd4b5d3c6 100644 --- a/applications/mp/pom.xml +++ b/applications/mp/pom.xml @@ -23,7 +23,7 @@ io.helidon.applications helidon-applications - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-mp @@ -58,6 +58,14 @@ org.hibernate.orm.tooling hibernate-enhance-maven-plugin ${version.plugin.hibernate-enhance} + + + + net.bytebuddy + byte-buddy + ${version.lib.bytebuddy} + + diff --git a/applications/parent/pom.xml b/applications/parent/pom.xml index 9dc148d6fee..a6d26ad323b 100644 --- a/applications/parent/pom.xml +++ b/applications/parent/pom.xml @@ -23,8 +23,7 @@ io.helidon.applications helidon-applications-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT io.helidon.applications helidon-applications @@ -42,8 +41,8 @@ 3.6.0 3.1.0 3.1.2 - 4.0.6 - 4.0.6 + 4.0.14 + 4.0.14 3.3.0 0.10.2 1.5.0.Final @@ -89,6 +88,7 @@ false true + ${project.build.outputDirectory} diff --git a/applications/pom.xml b/applications/pom.xml index e8a58ec9eb1..14ab1cc7a14 100644 --- a/applications/pom.xml +++ b/applications/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-dependencies - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../dependencies/pom.xml io.helidon.applications diff --git a/applications/se/pom.xml b/applications/se/pom.xml index e3fc3792428..4275245bb9f 100644 --- a/applications/se/pom.xml +++ b/applications/se/pom.xml @@ -23,7 +23,7 @@ io.helidon.applications helidon-applications - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-se diff --git a/archetypes/archetypes/pom.xml b/archetypes/archetypes/pom.xml index 99d392d90e2..c0e7f49f3f3 100644 --- a/archetypes/archetypes/pom.xml +++ b/archetypes/archetypes/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-archetype helidon-archetypes diff --git a/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/META-INF/beans.xml b/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/META-INF/beans.xml index fedfc8e6222..b0993408cd8 100644 --- a/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/META-INF/beans.xml +++ b/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/META-INF/beans.xml @@ -2,7 +2,7 @@ - \ No newline at end of file + diff --git a/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/logging.properties.mustache b/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/logging.properties.mustache index 40e77eff6ea..dac99e33461 100644 --- a/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/logging.properties.mustache +++ b/archetypes/archetypes/src/main/archetype/mp/common/files/src/main/resources/logging.properties.mustache @@ -20,6 +20,9 @@ java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$ # Quiet Weld org.jboss.level=WARNING +# Quiet Jersey wadl support +org.glassfish.jersey.server.wadl.level=SEVERE + # Component specific log levels #io.helidon.config.level=INFO #io.helidon.security.level=INFO diff --git a/archetypes/archetypes/src/main/archetype/mp/custom/database-outputs.xml b/archetypes/archetypes/src/main/archetype/mp/custom/database-outputs.xml index 359ddee62fe..d4ce58570fc 100644 --- a/archetypes/archetypes/src/main/archetype/mp/custom/database-outputs.xml +++ b/archetypes/archetypes/src/main/archetype/mp/custom/database-outputs.xml @@ -364,7 +364,7 @@ For details on an Oracle Docker image, see https://github.com/oracle/docker-imag com.oracle.database.jdbc - ucp + ucp11 runtime diff --git a/archetypes/archetypes/src/main/archetype/mp/oci/files/README-Description.md b/archetypes/archetypes/src/main/archetype/mp/oci/files/README-Description.md index 140d5e486bb..0e8ef560fe6 100644 --- a/archetypes/archetypes/src/main/archetype/mp/oci/files/README-Description.md +++ b/archetypes/archetypes/src/main/archetype/mp/oci/files/README-Description.md @@ -10,7 +10,7 @@ This example demonstrates Helidon's integration with Oracle Cloud Infrastructure 6. Logs published to OCI Logging Service 7. OCI Custom Logs Monitoring using Unified Monitoring Agent 8. Health and Liveness checks -9. Configuration profiles for switching between `config_file` and `instance_principal` configurations +9. Configuration profiles for switching between `config-file` and `instance-principals` configurations This project demonstrates OpenApi-driven development approach where the practice of designing and building APIs is done first, then creating the rest of an application around them is implemented next. Below are the modules that are part of this project: diff --git a/archetypes/archetypes/src/main/archetype/mp/oci/files/server/README.md.mustache b/archetypes/archetypes/src/main/archetype/mp/oci/files/server/README.md.mustache index 5bcd4695d18..67553ae3286 100644 --- a/archetypes/archetypes/src/main/archetype/mp/oci/files/server/README.md.mustache +++ b/archetypes/archetypes/src/main/archetype/mp/oci/files/server/README.md.mustache @@ -203,16 +203,16 @@ and currently has the following parameters: 7. `oci.logging.id` - Custom log id configured. Please see [Custom Logs Monitoring](#Custom-Logs-Monitoring) for more information. 8. `oci.auth-strategy` - Allows OCI client authentication mechanism to be specified and can either be an individual value or a list of authentication types separated by comma. If specified as a list, it will cycle through each until a -successful authentication is reached. This is currently set to `config_file,instance_principals,resource_principal` -which means that `config_file` will be tried first, then falls back to `instance_principals` if it fails, and -eventually falls back to `resource_principals` if `instance_principals` fails. The following are the different types +successful authentication is reached. This is currently set to `config-file,instance-principals,resource-principal` +which means that `config-file` will be tried first, then falls back to `instance-principals` if it fails, and +eventually falls back to `resource-principal` if `instance-principals` fails. The following are the different types of OCI client authentication: - * `config_file` - Uses user authentication set in ~/.oci/config + * `config-file` - Uses user authentication set in ~/.oci/config * `config` - Sets user authentication in the helidon config, i.e. microprofile-config.properties - * `instance_principals` - Uses the compute instance as the authentication and authorization principal. Please see + * `instance-principals` - Uses the compute instance as the authentication and authorization principal. Please see [Calling Services from an Instance](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm) for more details. - * `resource_principal` - Very similar to instance principal auth but uses OCI resources and services as the + * `resource-principal` - Very similar to instance principal auth but uses OCI resources and services as the authentication and authorization principal such as serverless functions. Please see [About Using Resource Principal to Access Oracle Cloud Infrastructure Resources](https://docs.oracle.com/en/cloud/paas/autonomous-database/adbsa/resource-principal-about.html#GUID-7EED5198-2C92-41B6-99DD-29F4187CABF5) for more information. @@ -236,4 +236,4 @@ to replace the configured value of `server.port` (currently set to `8080`), all `SERVER_PORT` environment variable to the desired value: ``` SERVER_PORT=8888 java -jar target/{{artifactId}}-server.jar - ``` \ No newline at end of file + ``` diff --git a/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-prod.properties b/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-prod.properties index 36aac4f263b..c476657edb4 100644 --- a/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-prod.properties +++ b/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-prod.properties @@ -1,2 +1,2 @@ # Oci Authentication method using Instance Principals -oci.auth-strategy=instance_principals +oci.auth-strategy=instance-principals diff --git a/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-test.properties b/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-test.properties index df287484fa5..8db91cce237 100644 --- a/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-test.properties +++ b/archetypes/archetypes/src/main/archetype/mp/oci/files/server/src/main/resources/META-INF/microprofile-config-test.properties @@ -1,2 +1,2 @@ # Oci Authentication method using User Credentials in an oci config file -oci.auth-strategy=config_file +oci.auth-strategy=config-file diff --git a/archetypes/archetypes/src/main/archetype/mp/oci/oci-mp.xml b/archetypes/archetypes/src/main/archetype/mp/oci/oci-mp.xml index 041f3d2d2c4..5b0d9ea67de 100644 --- a/archetypes/archetypes/src/main/archetype/mp/oci/oci-mp.xml +++ b/archetypes/archetypes/src/main/archetype/mp/oci/oci-mp.xml @@ -88,7 +88,7 @@ oci.monitoring.namespace= oci.logging.id= # OCI Authentication strategy -oci.auth-strategy=config_file,instance_principals,resource_principal +oci.auth-strategy=config-file,instance-principals,resource-principal ]]> @@ -342,7 +342,7 @@ principal when running it in an oci compute instance. ### Run the application -1. Default with no profile will use `config_file,instance_principals,resource_principal` authentication strategy +1. Default with no profile will use `config-file,instance-principals,resource-principal` authentication strategy ```bash java -jar server/target/{{artifactId}}.jar ``` diff --git a/archetypes/archetypes/src/main/archetype/se/custom/database-output.xml b/archetypes/archetypes/src/main/archetype/se/custom/database-output.xml index b1eac23deea..7163f50a199 100644 --- a/archetypes/archetypes/src/main/archetype/se/custom/database-output.xml +++ b/archetypes/archetypes/src/main/archetype/se/custom/database-output.xml @@ -201,14 +201,12 @@ docker run --rm --name mongo -p 27017:27017 mongo io.helidon.dbclient.health.DbClientHealthCheck - + + .addFeature(observe) diff --git a/archetypes/archetypes/src/main/archetype/se/se.xml b/archetypes/archetypes/src/main/archetype/se/se.xml index bd3dc5b22f8..10837bdf0f7 100644 --- a/archetypes/archetypes/src/main/archetype/se/se.xml +++ b/archetypes/archetypes/src/main/archetype/se/se.xml @@ -1,7 +1,7 @@ io.helidon.integrations.micronaut @@ -182,6 +207,16 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-docs + ${helidon.version} + io.helidon.security @@ -575,6 +610,11 @@ helidon-common-features ${helidon.version} + + io.helidon.common.concurrency + helidon-common-concurrency-limits + ${helidon.version} + @@ -698,6 +738,11 @@ helidon-microprofile-rest-client ${helidon.version} + + io.helidon.microprofile.rest-client-metrics + helidon-microprofile-rest-client-metrics + ${helidon.version} + @@ -746,6 +791,16 @@ helidon-lra-coordinator-narayana-client ${helidon.version} + + io.helidon.lra + helidon-lra-coordinator-server + ${helidon.version} + + + io.helidon.microprofile.lra + helidon-microprofile-lra-testing + ${helidon.version} + @@ -763,6 +818,11 @@ helidon-integrations-db-mysql ${helidon.version} + + io.helidon.integrations.db + helidon-integrations-db-pgsql + ${helidon.version} + io.helidon.integrations.cdi helidon-integrations-cdi-configurable @@ -858,6 +918,21 @@ helidon-integrations-oci ${helidon.version} + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-instance + ${helidon.version} + + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-resource + ${helidon.version} + + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-oke-workload + ${helidon.version} + io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-cdi @@ -1248,6 +1323,11 @@ helidon-webserver-service-common ${helidon.version} + + io.helidon.webserver + helidon-webserver-concurrency-limits + ${helidon.version} + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 @@ -1401,6 +1481,11 @@ + + io.helidon.service + helidon-service-metadata + ${helidon.version} + io.helidon.service helidon-service-registry @@ -1411,6 +1496,28 @@ helidon-service-codegen ${helidon.version} + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-api + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject + ${helidon.version} + + + + + io.helidon.metadata + helidon-metadata-hson + ${helidon.version} + diff --git a/builder/README.md b/builder/README.md index 14edca49d24..e7285949400 100644 --- a/builder/README.md +++ b/builder/README.md @@ -1,87 +1,281 @@ # Helidon Builder -This module is used by Helidon to generate types with builders (Prototypes) to be used in API of modules from a blueprint interface. +This module is used by Helidon to generate types with builders (Prototypes) to be used in API of modules from a blueprint +interface. There are two modules that are used: -- `helidon-builder-api` - module required in `compile` scope, contains annotations and APIs needed to write blueprints, and to build the generated code -- `helidon-builder-processor` - module to be placed on annotation processor path, generates the sources + +- `helidon-builder-api` - module required in `compile` scope, contains annotations and APIs needed to write blueprints, to compile + the generated code, and at runtime +- `helidon-builder-codegen` - module to be placed on annotation processor path, generates the sources There is one module useful for internal development -- `helidon-builder-tests-common-types` (located under `tests/common-types`) that contains blueprints for the types we use in `helidon-common-types`. As the common types module is used by the processor, we would end up with a cyclic dependency, so this allows us to generate the next iteration of common types (requires manual copying of the generated types) + +- `helidon-builder-tests-common-types` (located under `tests/common-types`) that contains blueprints for the types we use in + `helidon-common-types`. As the common types module is used by the processor, we would end up with a cyclic dependency, so this + allows us to generate the next iteration of common types (requires manual copying of the generated types) + +This document describes the main features and usage, there are further customization option. Kindly check usages of +`Prototype.Blueprint` in this repository, to see examples... + +Table of contents: + +- [Goals](#goals) - what we do +- [Non-Goals](#non-goals) - what we decided not to do +- [Rules](#rules) - what are the rules when using this module +- [Use Cases](#use-cases) - supported use cases +- [Getting Started](#getting-started) - set up your `pom.xml` and use this module +- [API](#api) - more details on available annotations and interfaces ## Goals Generate all required types for Helidon APIs with builders, that follow the same style (method names, required validation etc.). +Support for builders that can read options from Helidon configuration (`helidon-common-config`, and of course `helidon-config`). -The following list of features is currently supported: -- `Builder` also implements the interface of the type (all getters are available also on builder) -- `Type` options - interface returns `Type`, such an option MUST NOT be null in the built instance, there is a validation in place on calling the `build` or `buildPrototype` methods. Getters MAY return null on a builder -- `Optional` options - interface returns `Optional`, setters use just `Type`, there is a package local setter that accepts `Optional` as well, to support updating a builder from an existing instance -- `List` options - interface returns `List`, never null - if there is no configured value, empty string is returned -- `Set` options - similar to list -- `Map` options - key/value map, builders support any key/value types, but if configuration is used, the key must be a string -- "Singular" for collection based options, which adds setter for a single value (for `List algorithms()`, there would be the following setters: `algorithms(List)`, `addAlgorithms(List)`, `addAlgorithm(String)`) -- A type can be `@Configured`, which adds integration with Helidon common Config module, by adding a static factory method `create(io.helidon.common.Config)` to the generated type, as well as `config(Config)` method to the generated builder, that sets all options annotated with `@ConfiguredOption` from configuration (if present in the Config instance) -- Capability to update the builder before validation (decorator) -- Support for custom methods (`@Prototype.CustomMethods`) for factory methods, prototype methods, and builder methods +- We MUST NOT change bytecode of user classes +- We MUST NOT use reflection (everything is code generated) +- Support inheritance of prototypes (and of blueprints if in the same module) +- The generated code is the public API (and must have javadoc generated) +- The annotated blueprint interface is used to generate configuration metadata, and configuration documentation ( + see [Config metadata](../config/metadata/README.md)) +- Support prototypes configured from Helidon configuration (as an optional feature) +- Support additional methods to be generated (factory methods, prototype methods, builder methods) +- Support the following collections: `List`, `Set`, and `Map` +- Support for default values, for the most commonly used types to be typed explicitly (String, int, long, boolean etc.) +- Support for `enum` options ## Non-Goals -We are not building a general purpose solution, there are limitations that are known and will not be targetted: -- the solution expects that everything is single package - blueprints are required to be package local, which does not allow using built types across packages within a single module +We are not building a general purpose solution, there are limitations that are known and will not be targeted: + +- the solution expects that everything is single package - blueprints are required to be package local, which does not allow using + built types across packages within a single module - we only support interface based definition of blueprints (no classes) - we only support non-nullable options, instead of nullable, use `Optional` getters - implementation types of collections are fixed to `java.util.ArrayList`, `java.util.LinkedHashSet` and `java.util.LinkedHashMap` +## Rules + +There are a few rules we required and enforce: + +1. Blueprint MUST be an interface +2. Blueprint interface MUST be package private +3. Blueprint interface must have a name that ends with `Blueprint`; the name before `Blueprint` will be the name of the prototype +4. In case we use the blueprint -> prototype -> runtime type use case (see below): + 1. The blueprint must extend `Prototype.Factory` where `RuntimeType` is the type of the runtime object + 2. The runtime type must be annotated with `@RuntimeType.PrototypedBy(PrototypeBlueprint.class)` + 3. The runtime type must implement `RuntimeType.Api` + 4. The runtime type must have a `public static Prototype.Builder builder()` method implemented by user + 5. The runtime type must have a `public static RuntimeType create(Prototype)` method implemented by user + 6. The runtime type must have a `public static RuntimeType create(Consumer)` method implemented by user + +## Use Cases + +There are two use cases we cover: + +1. We need a type with a builder (we will use `Keys` as an example) +2. We need a runtime object, with a prototype with a builder (we will use `Retry` as an example) + +For both use cases, we need to understand how to create instances, obtain builders etc. + +### Type with a builder + +For this simple approach, the user facing API will look as: + +```java +Keys keys = Keys.builder() + .name("name") + .build(); +``` + +Configuration based API: + +```java +// the location of config is arbitrary, the API expects in ono the Keys node +Keys keys = Keys.create(config.get("keys")); +``` + +The "blueprint" of such type: + +```java +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; + +@Prototype.Blueprint +@Prototype.Configured // support method config(Config) on the builder, and a static create(Config) +interface KeysBlueprint { + @Option.Configured + String name(); +} +``` + +This will generate: + +- `Keys extends KeysBlueprint` interface +- `Keys.BuilderBase implements Keys` base builder, to support extensibility of `Keys` +- `Keys.Builder extends Keys.BuilderBase, io.helidon.common.Builder` inner class - the fluent API builder + for `Keys` +- `Keys.BuilderBase.KeysImpl implements Keys` implementation of `Keys` + +### Runtime object, blueprint, builder + +For this approach, the user facing API will be similar to: + +```java +Retry retry = Retry.builder() // method builder is not generated, must be hand coded, and will return "RetryPrototype.builder()" + .build(); // generated, creates a Retry instance through a factory method defined on Retry or on RetryPrototypeBlueprint + +RetryPrototype prototype = RetryPrototype.builder() + .buildPrototype(); // alternative build method to obtain the intermediate prototype object + +Retry retryFromPrototype = prototype.build(); // to runtime type +``` + +The "blueprint" of such type: + +```java +@Prototype.Blueprint +@Prototype.Configured // support method config(Config) on the builder, and a static create(Config) method if desired +intrerface RetryPrototypeBlueprint extends Prototype.Factory + + { + @Option.Configured + String name (); +} +``` + ## Getting Started -1. Write your interface that you want to have a builder for. + +1. Write your interface that you want to have a builder for + ```java interface MyConfigBeanBlueprint { String getName(); + boolean isEnabled(); + int getPort(); } ``` -2. Annotate your interface definition with `@Blueprint`, and optionally use `@ConfiguredOption`, `Singular` etc. to customize the getter methods. Remember to review the annotation attributes javadoc for any customizations. + +2. Annotate your interface definition with `@Blueprint`, and optionally use `@Prototype.Configured` and `@Option.Configured`, + `@Option.Singular` etc. to customize the getter methods. Remember to review the annotation attributes javadoc for any + customizations 3. Update your pom file to add annotation processor + ```xml ... - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - - - - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - - - - - - ... + + + + + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + ... ``` Generated types will be available under `./target/generated-sources/annotations` + * MyConfigBean (in the same package as MyConfigBeanBlueprint), with inner classes `BuilderBase` (for inheritance), `Builder`, * Support for `toString()`, `hashCode()`, and `equals()` are always included. * Support for `builder(MyConfigBean)` to create a new builder from an existing instance * Support for `from(MyConfigBean)` and `from(MyConfigBean.BuilderBase)` to update builder from an instance or builder -* Support for validation of required and non-nullable options (required options are options that have `@ConfiguredOption(required=true)`, non-nullable option is any option that is not primitive, collection, and does not return an `Optional`) +* Support for validation of required and non-nullable options (required options are options that have `@Option.Required` and are + primitive), non-nullable option is any option that is not primitive, collection, and does not return an `Optional`) * Support for builder decorator (`@Bluprint(decorator = MyDecorator.class)`), `class MyDecorator implements BuilderDecorator` + +## API + +The API has to sections: + +1. Inner types of `Prototype` class to configure `Blueprints`, and of `RuntimeType` to configure runtime types +2. Inner types of `Option` class to configure options + +### Prototype + +Annotations: + +| Annotation | Required | Description | +|-----------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Prototype.Blueprint` | `true` | Annotation on the blueprint interface is required to trigger annotation processing | +| `Prototype.Implement` | `false` | Add additional implemented types to the generated prototype | +| `Prototype.Annotated` | `false` | Allows adding an annotation (or annotations) to the generated class or methods | +| `Prototype.FactoryMethod` | `false` | Use in generated code to mark static factory methods, also can be used on blueprint factory methods to be used during code generation, and on custom methods to mark static methods to be added to prototype | +| `Prototype.Singular` | `false` | Used for lists, sets, and maps to add methods `add*`/`put*` in addition to the full collection setters | +| `Prototype.SameGeneric` | `false` | Use for maps, where we want a setter method to use the same generic type for key and for value (such as `Class key, T valuel`) | +| `Prototype.Redundant` | `false` | A redundant option will not be part of generated `toString`, `hashCode`, and `equals` methods (allows finer grained control) | +| `Prototype.Confidential` | `false` | A confidential option will not have value visible when `toString` is called, only if it is `null` or it has a value (`****`) | +| `Prototype.CustomMethods` | `false` | reference a class that will contain declarations (all static) of custom methods to be added to the generated code, can add prototype, builder, and factory methods | +| `Prototype.BuilderMethod` | `false` | Annotation to be placed on factory methods that are to be added to builder, first parameter is the `BuilderBase` of the prototype | +| `Prototype.PrototypeMethod` | `false` | Annotation to be placed on factory methods that are to be added to prototype, first parameter is the prototype instance | +| `RuntimeType.PrototypedBy` | `true` | Annotation on runtime type that is created from a `Prototype`, to map it to the prototype it can be created from, used to trigger annotation processor for validation | + +Interfaces: + +| Interface | Generated | Description | +|-------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `RuntimeType.Api` | `false` | runtime type must implement this interface to mark which prototype is used to create it | +| `Prototype.Factory` | `false` | if blueprint implements factory, it means the prototype is used to create a single runtime type and will have methods `build` and `get` both on builder an on prototype interface that create a new instance of the runtime object | +| `Prototype.BuilderDecorator` | `false` | custom decorator to modify builder before validation is done in method `build` | +| `Prototype.Api` | `true` | all prototypes implement this interface | +| `Prototype.Builder` | `true` | all prototype builders implement this interface, defines method `buildPrototype` | +| `Prototype.ConfiguredBuilder` | `true` | all prototype builders that support configuration implement this interface, defines method `config(Config)` | + +### Option + +| Annotation | Description | +|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@Option.Singular` | For collection based options. Adds setter for a single value (for `List algorithms()`, there would be the following setters: `algorithms(List)`, `addAlgorithms(List)`, `addAlgorithm(String)`) | +| `@Option.Configured` | For options that are configured from config (must be explicitly marked, default is not-configured), also ignored unless `@Prototype.Configured` is specified on the blueprint interface | +| `@Option.Required` | We can recognize required options through signature in most cases (any option that does not return an `Optional` and does not have a default value); this option is useful for primitive types, where we need an explicit value set, rather than using the primitive's default value | +| `@Option.Provider` | Satisfied by a provider implementation, see javadoc for details | +| `@Option.AllowedValues` | Allowed values for the property, not required for `enum`, where we create this automatically, though we can configure description of each value (works automatically for `enum` defined in the same module); the description is used for generated documentation | +| `@Option.SameGeneric` | Advanced configuration of a Map, where the map accepts two typed values, and we must use the same generic on setters (such as `Map, Object>` - ` Builder put(Class, T)`) | +| `@Option.Redundant` | Marks an option that is not used by equals and hashCode methods | +| `@Option.Confidential` | Marks an option that will not be visible in `toString()` | +| `@Option.Deprecated` | Marks a deprecated option that has a replacement option in this builder, use Java's deprecation for other cases, they will be honored in the generated code | +| `@Option.Type` | Explicitly defined type of a property (may include generics), in case the type is code generated in the current module, and we cannot obtain the correct information from the annotation processing environment | +| `@Option.Decorator` | Support for field decoration (to do side-effects on setter call) | + +To configure default value(s) of an option, one of the following annotations can be used (mutually exclusive!). +Most defaults support an array, to provide default values for collections. + +| Annotation | Description | +|--------------------------|----------------------------------------------------------------------------------------------------| +| `@Option.Default` | Default value(s) that are `String` or we support coercion to the correct type (`enum`, `Duration`) | +| `@Option.DefaultInt` | Default value(s) that are `int` | +| `@Option.DefaultLong` | Default value(s) that are `long` | +| `@Option.DefaultDouble` | Default value(s) that are `double` | +| `@Option.DefaultBoolean` | Default value(s) that are `boolean` | +| `@Option.DefaultMethod` | Static method to invoke to obtain a default value | +| `@Option.DefaultCode` | Source code to add to the generated assignment, single line only supported | diff --git a/builder/api/README.md b/builder/api/README.md index e48166f5d40..fc01ae5cf0e 100644 --- a/builder/api/README.md +++ b/builder/api/README.md @@ -1,148 +1,3 @@ -# Builder +# Builder API -## Description - -There are two use cases we cover: - -1. We need a type with a builder (we will use `Keys` as an example) -2. We need a runtime object, with a prototype with a builder (we will use `Retry` as an example) - -For both use cases, we need to understand how to create instances, obtain builders etc. - -### Type with a builder - -For this simple approach, the user facing API will look as it does now: - -```java -Keys keys=Keys.builder() - .name("name") - .build(); -``` - -The "blueprint" of such type: - -```java -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; - -@Prototype.Blueprint -@Configured // support method config(Config) on the builder, and a static create(Config) -interface KeysBlueprint{ - @ConfiguredOption(required = true) - String name(); - } -``` - -This will generate: - -- `Keys extends KeysBlueprint` interface -- `Keys.BuilderBase implements Keys` base builder, to support extensibility of `Keys` -- `Keys.Builder extends Keys.BuilderBase, io.helidon.common.Builder` inner class - the fluent API builder - for `Keys` -- `Keys.BuilderBase.KeysImpl implements Keys` implementation of `Keys` - -### Runtime object, blueprint, builder - -For this approach, the user facing API will be similar to what we do now: - -```java -Retry retry=Retry.builder() // method builder is not generated, must be hand coded, and will return "RetryPrototype.builder()" - .build(); // generated, creates a Retry instance through a factory method defined on Retry or on RetryPrototypeBlueprint - - RetryPrototype prototype=RetryPrototype.builder() - .buildPrototype(); // alternative build method to obtain the intermediate prototype object - - Retry retryFromSetup=prototype.build(); // to runtime type -``` - -The "blueprint" of such type: - -```java -@Prototype.Blueprint -@Configured // support method config(Config) on the builder, and a static create(Config) method if desired -intrerface RetryPrototypeBlueprint extends Prototype.Factory { -@ConfiguredOption(required = true) - String name(); - } -``` - -## Types, interfaces - -Annotations: - -| Annotation | Required | Description | -|-----------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Prototype.Blueprint` | `true` | Annotation on the blueprint interface is required to trigger annotation processing | -| `Prototype.Implement` | `false` | Add additional implemented types to the generated prototype | -| `Prototype.Annotated` | `false` | Allows adding an annotation (or annotations) to the generated class or methods | -| `Prototype.FactoryMethod` | `false` | Use in generated code to mark static factory methods, also can be used on blueprint factory methods to be used during code generation, and on custom methods to mark static methods to be added to prototype | -| `Prototype.Singular` | `false` | Used for lists, sets, and maps to add methods `add*`/`put*` in addition to the full collection setters | -| `Prototype.SameGeneric` | `false` | Use for maps, where we want a setter method to use the same generic type for key and for value (such as `Class key, T valuel`) | -| `Prototype.Redundant` | `false` | A redundant option will not be part of generated `toString`, `hashCode`, and `equals` methods (allows finer grained control) | -| `Prototype.Confidential` | `false` | A confidential option will not have value visible when `toString` is called, only if it is `null` or it has a value (`****`) | -| `Prototype.CustomMethods` | `false` | reference a class that will contain declarations (all static) of custom methods to be added to the generated code, can add prototype, builder, and factory methods | -| `Prototype.BuilderMethod` | `false` | Annotation to be placed on factory methods that are to be added to builder, first parameter is the `BuilderBase` of the prototype | -| `Prototype.PrototypeMethod` | `false` | Annotation to be placed on factory methods that are to be added to prototype, first parameter is the prototype instance | -| `RuntimeType.PrototypedBy` | `true` | Annotation on runtime type that is created from a `Prototype`, to map it to the prototype it can be created from, used to trigger annotation processor for validation | - -Interfaces: - -| Interface | Generated | Description | -|-------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `RuntimeType.Api` | `false` | runtime type must implement this interface to mark which prototype is used to create it | -| `Prototype.Factory` | `false` | if blueprint implements factory, it means the prototype is used to create a single runtime type and will have methods `build` and `get` both on builder an on prototype interface that create a new instance of the runtime object | -| `Prototype.BuilderDecorator` | `false` | custom decorator to modify builder before validation is done in method `build` | -| `Prototype.Api` | `true` | all prototypes implement this interface | -| `Prototype.Builder` | `true` | all prototype builders implement this interface, defines method `buildPrototype` | -| `Prototype.ConfiguredBuilder` | `true` | all prototype builders that support configuration implement this interface, defines method `config(Config)` | - -## Configured providers - -We can define a configured option as follows: -`@ConfiguredOption(key = "security-providers", provider = true, providerType = SecurityProviderProvider.class, providerDiscoverServices = false)` - -Rules: - -1. `providerType` MUST extend `io.helidon.common.config.ConfiguredProvider` -2. The method MUST return a `List` of the type the provider creates, so in this case we consider the `SecurityProviderProvider` - to be capable of creating a `SecurityProvider` instance from configuration, so the return type would - be `List`, where `SecurityProvider extends NamedService` and - `SecurityProviderProvider extends ConfiguredProvider` - -This will expect the following configuration: - -```yaml -security-providers: - discover-services: true # optional, to override "providerDiscoverServices" option - providers: - - name: "my-provider" - type: "http-basic" - enabled: true -``` - -The generated code will read all nodes under `providers` and map them to an instance. - -## Naming rules - -Part of the naming rules is constant, part depends on whether we use two or three types, as mentioned above. - -### Blueprint name -Blueprint MUST be package local, and MUST be named with a `Blueprint` suffix. The part of the name before the suffix will be the prototype name. - -### Blueprint -> Prototype -For cases, where the `Prototype` is the target desired type (such as `TypeName`, `Keys`), the prototype name is a name of the type we represent (no suffixes, prefixes etc.). - -Example: `TypeName` would have the following structure (as can be seen in the [builder/tests/common-types](../tests/common-types)): - -- `TypeNameBlueprint` - the definition of the type -- `TypeName` - the generated type to be used as an API - -### Blueprint -> Prototype -> Runtime type -For cases, where the `Prototype` serves as a configuration object of a runtime type (such as `WebServerConfig`, `RetryConfig`), -the prototype name should have a `Config` suffix, and the runtime type is a name of the type we represent. - -Example: `Retry` would have the following structure (can be seen in Fault Tolerance): - -- `RetryConfigBlueprint` - the definition of the config -- `RetryConfig` - the prototype -- `Retry` - the runtime type \ No newline at end of file +This document is merged into parent readme, see [Builder](../README.md). diff --git a/builder/api/pom.xml b/builder/api/pom.xml index 7af94469439..7e6a6158597 100644 --- a/builder/api/pom.xml +++ b/builder/api/pom.xml @@ -24,8 +24,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/api/src/main/java/io/helidon/builder/api/Option.java b/builder/api/src/main/java/io/helidon/builder/api/Option.java index f1ac6d14a0b..96f6143be93 100644 --- a/builder/api/src/main/java/io/helidon/builder/api/Option.java +++ b/builder/api/src/main/java/io/helidon/builder/api/Option.java @@ -432,7 +432,7 @@ private Option() { * This is useful for example when setting a compound option, where we need to set additional options on this builder. *

* Decorator on collection based options will be ignored. - * Decorator on optional values must accept an option (as it would be called both from the setter and unset methods). + * Decorator on optional values must accept an optional (as it would be called both from the setter and unset methods). */ @Target(ElementType.METHOD) // note: class retention needed for cases when derived builders are inherited across modules diff --git a/builder/codegen/pom.xml b/builder/codegen/pom.xml index d701cc9bd9b..164695c1b40 100644 --- a/builder/codegen/pom.xml +++ b/builder/codegen/pom.xml @@ -23,8 +23,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java index c8a256d1b18..22e13bb88b5 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java @@ -122,153 +122,13 @@ public void processingOver(RoundContext roundContext) { } } - private void updateServiceLoaderResource() { - CodegenFiler filer = ctx.filer(); - FilerTextResource serviceLoaderResource = filer.textResource("META-INF/helidon/service.loader"); - List lines = new ArrayList<>(serviceLoaderResource.lines()); - if (lines.isEmpty()) { - lines.add("# List of service contracts we want to support either from service registry, or from service loader"); - } - for (String serviceLoaderContract : this.serviceLoaderContracts) { - if (!lines.contains(serviceLoaderContract)) { - lines.add(serviceLoaderContract); - } - } - - serviceLoaderResource.lines(lines); - serviceLoaderResource.write(); - } - - private void process(RoundContext roundContext, TypeInfo blueprint) { - TypeContext typeContext = TypeContext.create(ctx, blueprint); - AnnotationDataBlueprint blueprintDef = typeContext.blueprintData(); - AnnotationDataConfigured configuredData = typeContext.configuredData(); - TypeContext.PropertyData propertyData = typeContext.propertyData(); - TypeContext.TypeInformation typeInformation = typeContext.typeInfo(); - CustomMethods customMethods = typeContext.customMethods(); - - TypeInfo typeInfo = typeInformation.blueprintType(); - TypeName prototype = typeContext.typeInfo().prototype(); - String ifaceName = prototype.className(); - List typeGenericArguments = blueprintDef.typeArguments(); - String typeArgumentString = createTypeArgumentString(typeGenericArguments); - - // prototype interface (with inner class Builder) - ClassModel.Builder classModel = ClassModel.builder() - .type(prototype) - .classType(ElementKind.INTERFACE) - .copyright(CodegenUtil.copyright(GENERATOR, - typeInfo.typeName(), - prototype)); - - String javadocString = blueprintDef.javadoc(); - List typeArguments = new ArrayList<>(); - if (javadocString == null) { - classModel.description("Interface generated from definition. Please add javadoc to the definition interface."); - typeGenericArguments.forEach(arg -> typeArguments.add(TypeArgument.builder() - .token(arg.className()) - .build())); - } else { - Javadoc javadoc = Javadoc.parse(blueprintDef.javadoc()); - classModel.javadoc(javadoc); - typeGenericArguments.forEach(arg -> { - TypeArgument.Builder tokenBuilder = TypeArgument.builder().token(arg.className()); - if (javadoc.genericsTokens().containsKey(arg.className())) { - tokenBuilder.description(javadoc.genericsTokens().get(arg.className())); - } - typeArguments.add(tokenBuilder.build()); - }); - } - typeArguments.forEach(classModel::addGenericArgument); - - if (blueprintDef.builderPublic()) { - classModel.addJavadocTag("see", "#builder()"); - } - if (!propertyData.hasRequired() && blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { - classModel.addJavadocTag("see", "#create()"); - } - - typeContext.typeInfo() - .annotationsToGenerate() - .forEach(annotation -> classModel.addAnnotation(io.helidon.codegen.classmodel.Annotation.parse(annotation))); - - classModel.addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, - typeInfo.typeName(), - prototype, - "1", - "")); - - if (typeContext.blueprintData().prototypePublic()) { - classModel.accessModifier(AccessModifier.PUBLIC); - } else { - classModel.accessModifier(AccessModifier.PACKAGE_PRIVATE); - } - blueprintDef.extendsList() - .forEach(classModel::addInterface); - - generateCustomConstants(customMethods, classModel); - - TypeName builderTypeName = TypeName.builder() - .from(TypeName.create(prototype.fqName() + ".Builder")) - .typeArguments(prototype.typeArguments()) - .build(); - - - // static Builder builder() - addBuilderMethod(classModel, builderTypeName, typeArguments, ifaceName); - - // static Builder builder(T instance) - addCopyBuilderMethod(classModel, builderTypeName, prototype, typeArguments, ifaceName, typeArgumentString); - - // static T create(Config config) - addCreateFromConfigMethod(blueprintDef, - configuredData, - prototype, - typeArguments, - ifaceName, - typeArgumentString, - classModel); - - // static X create() - addCreateDefaultMethod(blueprintDef, propertyData, classModel, prototype, ifaceName, typeArgumentString, typeArguments); - - generateCustomMethods(customMethods, classModel); - - // abstract class BuilderBase... - GenerateAbstractBuilder.generate(classModel, - typeInformation.prototype(), - typeInformation.runtimeObject().orElseGet(typeInformation::prototype), - typeArguments, - typeContext); - // class Builder extends BuilderBase ... - GenerateBuilder.generate(classModel, - typeInformation.prototype(), - typeInformation.runtimeObject().orElseGet(typeInformation::prototype), - typeArguments, - typeContext.blueprintData().isFactory(), - typeContext); - - roundContext.addGeneratedType(prototype, - classModel, - blueprint.typeName(), - blueprint.originatingElement().orElse(blueprint.typeName())); - - if (typeContext.typeInfo().supportsServiceRegistry() && typeContext.propertyData().hasProvider()) { - for (PrototypeProperty property : typeContext.propertyData().properties()) { - if (property.configuredOption().provider()) { - this.serviceLoaderContracts.add(property.configuredOption().providerType().genericTypeName().fqName()); - } - } - } - } - private static void addCreateDefaultMethod(AnnotationDataBlueprint blueprintDef, - TypeContext.PropertyData propertyData, - ClassModel.Builder classModel, - TypeName prototype, - String ifaceName, - String typeArgumentString, - List typeArguments) { + TypeContext.PropertyData propertyData, + ClassModel.Builder classModel, + TypeName prototype, + String ifaceName, + String typeArgumentString, + List typeArguments) { if (blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { /* static X create() @@ -287,12 +147,12 @@ static X create() } private static void addCreateFromConfigMethod(AnnotationDataBlueprint blueprintDef, - AnnotationDataConfigured configuredData, - TypeName prototype, - List typeArguments, - String ifaceName, - String typeArgumentString, - ClassModel.Builder classModel) { + AnnotationDataConfigured configuredData, + TypeName prototype, + List typeArguments, + String ifaceName, + String typeArgumentString, + ClassModel.Builder classModel) { if (blueprintDef.createFromConfigPublic() && configuredData.configured()) { Method.Builder method = Method.builder() .name("create") @@ -318,11 +178,11 @@ private static void addCreateFromConfigMethod(AnnotationDataBlueprint blueprintD } private static void addCopyBuilderMethod(ClassModel.Builder classModel, - TypeName builderTypeName, - TypeName prototype, - List typeArguments, - String ifaceName, - String typeArgumentString) { + TypeName builderTypeName, + TypeName prototype, + List typeArguments, + String ifaceName, + String typeArgumentString) { classModel.addMethod(builder -> { builder.isStatic(true) .name("builder") @@ -337,9 +197,9 @@ private static void addCopyBuilderMethod(ClassModel.Builder classModel, } private static void addBuilderMethod(ClassModel.Builder classModel, - TypeName builderTypeName, - List typeArguments, - String ifaceName) { + TypeName builderTypeName, + List typeArguments, + String ifaceName) { classModel.addMethod(builder -> { builder.isStatic(true) .name("builder") @@ -366,8 +226,27 @@ private static void generateCustomConstants(CustomMethods customMethods, ClassMo } } - private static void generateCustomMethods(CustomMethods customMethods, ClassModel.Builder classModel) { + private static void generateCustomMethods(ClassModel.Builder classModel, + TypeName builderTypeName, + TypeName prototype, + CustomMethods customMethods) { for (CustomMethods.CustomMethod customMethod : customMethods.factoryMethods()) { + TypeName typeName = customMethod.declaredMethod().returnType(); + // there is a chance the typeName does not have a package (if "forward referenced"), + // in that case compare just by classname (leap of faith...) + if (typeName.packageName().isBlank()) { + String className = typeName.className(); + if (!( + className.equals(prototype.className()) + || className.equals(builderTypeName.className()))) { + // based on class names + continue; + } + } else if (!(typeName.equals(prototype) || typeName.equals(builderTypeName))) { + // we only generate custom factory methods if they return prototype or builder + continue; + } + // prototype definition - custom static factory methods // static TypeName create(Type type); CustomMethods.Method generated = customMethod.generatedMethod().method(); @@ -415,6 +294,159 @@ private static void generateCustomMethods(CustomMethods customMethods, ClassMode } } + private void updateServiceLoaderResource() { + CodegenFiler filer = ctx.filer(); + FilerTextResource serviceLoaderResource = filer.textResource("META-INF/helidon/service.loader"); + List lines = new ArrayList<>(serviceLoaderResource.lines()); + if (lines.isEmpty()) { + lines.add("# List of service contracts we want to support either from service registry, or from service loader"); + } + boolean modified = false; + for (String serviceLoaderContract : this.serviceLoaderContracts) { + if (!lines.contains(serviceLoaderContract)) { + modified = true; + lines.add(serviceLoaderContract); + } + } + + if (modified) { + serviceLoaderResource.lines(lines); + serviceLoaderResource.write(); + } + } + + private void process(RoundContext roundContext, TypeInfo blueprint) { + TypeContext typeContext = TypeContext.create(ctx, blueprint); + AnnotationDataBlueprint blueprintDef = typeContext.blueprintData(); + AnnotationDataConfigured configuredData = typeContext.configuredData(); + TypeContext.PropertyData propertyData = typeContext.propertyData(); + TypeContext.TypeInformation typeInformation = typeContext.typeInfo(); + CustomMethods customMethods = typeContext.customMethods(); + + TypeInfo typeInfo = typeInformation.blueprintType(); + TypeName prototype = typeContext.typeInfo().prototype(); + String ifaceName = prototype.className(); + List typeGenericArguments = blueprintDef.typeArguments(); + String typeArgumentString = createTypeArgumentString(typeGenericArguments); + + // prototype interface (with inner class Builder) + ClassModel.Builder classModel = ClassModel.builder() + .type(prototype) + .classType(ElementKind.INTERFACE) + .copyright(CodegenUtil.copyright(GENERATOR, + typeInfo.typeName(), + prototype)); + + String javadocString = blueprintDef.javadoc(); + List typeArguments = new ArrayList<>(); + Javadoc javadoc; + if (javadocString == null) { + javadoc = Javadoc.parse("Interface generated from definition. Please add javadoc to the " + + "definition interface."); + } else { + javadoc = Javadoc.parse(blueprintDef.javadoc()); + } + classModel.javadoc(javadoc); + + typeGenericArguments.forEach(arg -> { + TypeArgument.Builder tokenBuilder = TypeArgument.builder() + .token(arg.className()); + if (!arg.upperBounds().isEmpty()) { + arg.upperBounds().forEach(tokenBuilder::addBound); + } + if (javadoc.genericsTokens().containsKey(arg.className())) { + tokenBuilder.description(javadoc.genericsTokens().get(arg.className())); + } + typeArguments.add(tokenBuilder.build()); + }); + + List typeArgumentNames = typeArguments.stream() + .map(it -> TypeName.createFromGenericDeclaration(it.className())) + .collect(Collectors.toList()); + typeArguments.forEach(classModel::addGenericArgument); + + if (blueprintDef.builderPublic()) { + classModel.addJavadocTag("see", "#builder()"); + } + if (!propertyData.hasRequired() && blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { + classModel.addJavadocTag("see", "#create()"); + } + + typeContext.typeInfo() + .annotationsToGenerate() + .forEach(annotation -> classModel.addAnnotation(io.helidon.codegen.classmodel.Annotation.parse(annotation))); + + classModel.addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + typeInfo.typeName(), + prototype, + "1", + "")); + + if (typeContext.blueprintData().prototypePublic()) { + classModel.accessModifier(AccessModifier.PUBLIC); + } else { + classModel.accessModifier(AccessModifier.PACKAGE_PRIVATE); + } + blueprintDef.extendsList() + .forEach(classModel::addInterface); + + generateCustomConstants(customMethods, classModel); + + TypeName builderTypeName = TypeName.builder() + .from(TypeName.create(prototype.fqName() + ".Builder")) + .typeArguments(prototype.typeArguments()) + .build(); + + // static Builder builder() + addBuilderMethod(classModel, builderTypeName, typeArguments, ifaceName); + + // static Builder builder(T instance) + addCopyBuilderMethod(classModel, builderTypeName, prototype, typeArguments, ifaceName, typeArgumentString); + + // static T create(Config config) + addCreateFromConfigMethod(blueprintDef, + configuredData, + prototype, + typeArguments, + ifaceName, + typeArgumentString, + classModel); + + // static X create() + addCreateDefaultMethod(blueprintDef, propertyData, classModel, prototype, ifaceName, typeArgumentString, typeArguments); + + generateCustomMethods(classModel, builderTypeName, prototype, customMethods); + + // abstract class BuilderBase... + GenerateAbstractBuilder.generate(classModel, + typeInformation.prototype(), + typeInformation.runtimeObject().orElseGet(typeInformation::prototype), + typeArguments, + typeArgumentNames, + typeContext); + // class Builder extends BuilderBase ... + GenerateBuilder.generate(classModel, + typeInformation.prototype(), + typeInformation.runtimeObject().orElseGet(typeInformation::prototype), + typeArguments, + typeArgumentNames, + typeContext.blueprintData().isFactory(), + typeContext); + + roundContext.addGeneratedType(prototype, + classModel, + blueprint.typeName(), + blueprint.originatingElementValue()); + + if (typeContext.typeInfo().supportsServiceRegistry() && typeContext.propertyData().hasProvider()) { + for (PrototypeProperty property : typeContext.propertyData().properties()) { + if (property.configuredOption().provider()) { + this.serviceLoaderContracts.add(property.configuredOption().providerType().genericTypeName().fqName()); + } + } + } + } + private Collection addBlueprintsForValidation(Set blueprints) { List result = new ArrayList<>(); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java index cfd9931b2c3..cadf5f55a2e 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java @@ -84,17 +84,17 @@ static FactoryMethods create(CodegenContext ctx, private static Optional builder(CodegenContext ctx, TypeHandler typeHandler, Set builderCandidates) { - if (typeHandler.actualType().equals(OBJECT)) { + if (typeHandler.actualType().equals(OBJECT) + || typeHandler.actualType().primitive() + || typeHandler.actualType().generic()) { return Optional.empty(); } + builderCandidates.add(typeHandler.actualType()); FactoryMethod found = null; FactoryMethod secondary = null; for (TypeName builderCandidate : builderCandidates) { - if (typeHandler.actualType().primitive()) { - // primitive methods do not have builders - continue; - } + TypeInfo typeInfo = ctx.typeInfo(builderCandidate.genericTypeName()).orElse(null); if (typeInfo == null) { if (secondary == null) { diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java index 27a75cd7f0e..4da0d457c6f 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java @@ -59,6 +59,7 @@ static void generate(ClassModel.Builder classModel, TypeName prototype, TypeName runtimeType, List typeArguments, + List typeArgumentNames, TypeContext typeContext) { Optional superType = typeContext.typeInfo() .superPrototype(); @@ -73,7 +74,7 @@ static void generate(ClassModel.Builder classModel, .description("type of the builder extending this abstract builder") .bound(TypeName.builder() .from(TypeName.create(prototype.fqName() + ".BuilderBase")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) .build())) @@ -107,7 +108,7 @@ static void generate(ClassModel.Builder classModel, // method "from(prototype)" fromInstanceMethod(builder, typeContext, prototype); - fromBuilderMethod(builder, typeContext, typeArguments); + fromBuilderMethod(builder, typeContext, typeArgumentNames); // method preBuildPrototype() - handles providers, decorator preBuildPrototypeMethod(builder, typeContext); @@ -126,7 +127,7 @@ static void generate(ClassModel.Builder classModel, true); // before the builder class is finished, we also generate a protected implementation - generatePrototypeImpl(builder, typeContext, typeArguments); + generatePrototypeImpl(builder, typeContext, typeArguments, typeArgumentNames); }); } @@ -431,7 +432,8 @@ private static void fromInstanceMethod(InnerClass.Builder builder, TypeContext t private static void fromBuilderMethod(InnerClass.Builder classBuilder, TypeContext typeContext, - List arguments) { + List arguments) { + TypeName prototype = typeContext.typeInfo().prototype(); TypeName parameterType = TypeName.builder() .from(TypeName.create(prototype.fqName() + ".BuilderBase")) @@ -823,7 +825,8 @@ private static void requiredValidation(Method.Builder validateBuilder, TypeConte private static void generatePrototypeImpl(InnerClass.Builder classBuilder, TypeContext typeContext, - List typeArguments) { + List typeArguments, + List typeArgumentNames) { Optional superPrototype = typeContext.typeInfo() .superPrototype(); TypeName prototype = typeContext.typeInfo().prototype(); @@ -864,7 +867,7 @@ private static void generatePrototypeImpl(InnerClass.Builder classBuilder, .addParameter(param -> param.name("builder") .type(TypeName.builder() .from(TypeName.create(ifaceName + ".BuilderBase")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .addTypeArgument(TypeArgument.create("?")) .addTypeArgument(TypeArgument.create("?")) .build()) diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java index 6a5e780bbf3..21438e8f9a9 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java @@ -40,12 +40,13 @@ static void generate(ClassModel.Builder classBuilder, TypeName prototype, TypeName runtimeType, List typeArguments, + List typeArgumentNames, boolean isFactory, TypeContext typeContext) { classBuilder.addInnerClass(builder -> { TypeName builderType = TypeName.builder() .from(TypeName.create(prototype.fqName() + ".Builder")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .build(); typeArguments.forEach(builder::addGenericArgument); builder.name("Builder") @@ -53,7 +54,7 @@ static void generate(ClassModel.Builder classBuilder, .description("Fluent API builder for {@link " + runtimeType.className() + "}.") .superType(TypeName.builder() .from(TypeName.create(prototype.fqName() + ".BuilderBase")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .addTypeArgument(builderType) .addTypeArgument(prototype) .build()) diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java index f4b2954d52e..2488dea8a4c 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java @@ -78,7 +78,13 @@ static PrototypeProperty create(CodegenContext ctx, boolean sameGeneric = element.hasAnnotation(Types.OPTION_SAME_GENERIC); // to help with defaults, setters, config mapping etc. - TypeHandler typeHandler = TypeHandler.create(name, getterName, setterName, returnType, sameGeneric); + TypeHandler typeHandler = TypeHandler.create(blueprint.typeName(), + element, + name, + getterName, + setterName, + returnType, + sameGeneric); // all information from @ConfiguredOption annotation AnnotationDataOption configuredOption = AnnotationDataOption.create(typeHandler, element); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java index 0754e83079f..c67e0d0ef6f 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java @@ -16,55 +16,113 @@ package io.helidon.builder.codegen; +import java.net.URI; +import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenValidator; import io.helidon.codegen.classmodel.ContentBuilder; import io.helidon.codegen.classmodel.Field; import io.helidon.codegen.classmodel.InnerClass; import io.helidon.codegen.classmodel.Javadoc; import io.helidon.codegen.classmodel.Method; +import io.helidon.common.Size; import io.helidon.common.types.AccessModifier; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT; +import static io.helidon.common.types.TypeNames.BOXED_BOOLEAN; +import static io.helidon.common.types.TypeNames.BOXED_BYTE; +import static io.helidon.common.types.TypeNames.BOXED_CHAR; +import static io.helidon.common.types.TypeNames.BOXED_DOUBLE; +import static io.helidon.common.types.TypeNames.BOXED_FLOAT; +import static io.helidon.common.types.TypeNames.BOXED_INT; +import static io.helidon.common.types.TypeNames.BOXED_LONG; +import static io.helidon.common.types.TypeNames.BOXED_SHORT; +import static io.helidon.common.types.TypeNames.BOXED_VOID; +import static io.helidon.common.types.TypeNames.PRIMITIVE_BOOLEAN; +import static io.helidon.common.types.TypeNames.PRIMITIVE_BYTE; +import static io.helidon.common.types.TypeNames.PRIMITIVE_CHAR; +import static io.helidon.common.types.TypeNames.PRIMITIVE_DOUBLE; +import static io.helidon.common.types.TypeNames.PRIMITIVE_FLOAT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_INT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_LONG; +import static io.helidon.common.types.TypeNames.PRIMITIVE_SHORT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_VOID; class TypeHandler { + private static final Map BOXED_TO_PRIMITIVE = Map.of( + BOXED_BOOLEAN, PRIMITIVE_BOOLEAN, + BOXED_BYTE, PRIMITIVE_BYTE, + BOXED_SHORT, PRIMITIVE_SHORT, + BOXED_INT, PRIMITIVE_INT, + BOXED_LONG, PRIMITIVE_LONG, + BOXED_CHAR, PRIMITIVE_CHAR, + BOXED_FLOAT, PRIMITIVE_FLOAT, + BOXED_DOUBLE, PRIMITIVE_DOUBLE, + BOXED_VOID, PRIMITIVE_VOID + ); + + private final TypeName enclosingType; + private final TypedElementInfo annotatedMethod; private final String name; private final String getterName; private final String setterName; private final TypeName declaredType; - TypeHandler(String name, String getterName, String setterName, TypeName declaredType) { + TypeHandler(TypeName enclosingType, + TypedElementInfo annotatedMethod, + String name, + String getterName, + String setterName, + TypeName declaredType) { + this.enclosingType = enclosingType; + this.annotatedMethod = annotatedMethod; this.name = name; this.getterName = getterName; this.setterName = setterName; this.declaredType = declaredType; } - static TypeHandler create(String name, String getterName, String setterName, TypeName returnType, boolean sameGeneric) { + static TypeHandler create(TypeName blueprintType, + TypedElementInfo annotatedMethod, + String name, + String getterName, + String setterName, + TypeName returnType, + boolean sameGeneric) { if (TypeNames.OPTIONAL.equals(returnType)) { - return new TypeHandlerOptional(name, getterName, setterName, returnType); + return new TypeHandlerOptional(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } if (TypeNames.SUPPLIER.equals(returnType)) { - return new TypeHandlerSupplier(name, getterName, setterName, returnType); + return new TypeHandlerSupplier(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } + if (TypeNames.SET.equals(returnType)) { - return new TypeHandlerSet(name, getterName, setterName, returnType); + checkTypeArgsSizeAndTypes(annotatedMethod, returnType, TypeNames.SET, 1); + return new TypeHandlerSet(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } if (TypeNames.LIST.equals(returnType)) { - return new TypeHandlerList(name, getterName, setterName, returnType); + checkTypeArgsSizeAndTypes(annotatedMethod, returnType, TypeNames.LIST, 1); + return new TypeHandlerList(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } if (TypeNames.MAP.equals(returnType)) { - return new TypeHandlerMap(name, getterName, setterName, returnType, sameGeneric); + checkTypeArgsSizeAndTypes(annotatedMethod, returnType, TypeNames.MAP, 2); + return new TypeHandlerMap(blueprintType, annotatedMethod, name, getterName, setterName, returnType, sameGeneric); } - return new TypeHandler(name, getterName, setterName, returnType); + return new TypeHandler(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } static AccessModifier setterAccessModifier(AnnotationDataOption configured) { @@ -75,6 +133,12 @@ static TypeName toWildcard(TypeName typeName) { if (typeName.wildcard()) { return typeName; } + if (typeName.generic()) { + return TypeName.builder() + .className(typeName.className()) + .wildcard(true) + .build(); + } return TypeName.builder(typeName).wildcard(true).build(); } @@ -157,7 +221,15 @@ Consumer> toDefaultValue(String defaultValue) { .addContent(defaultValue) .addContent("\""); } + if (TypeNames.SIZE.equals(typeName)) { + CodegenValidator.validateSize(enclosingType, annotatedMethod, OPTION_DEFAULT, "value", defaultValue); + return content -> content.addContent(Size.class) + .addContent(".parse(\"") + .addContent(defaultValue) + .addContent("\")"); + } if (TypeNames.DURATION.equals(typeName)) { + CodegenValidator.validateDuration(enclosingType, annotatedMethod, OPTION_DEFAULT, "value", defaultValue); return content -> content.addContent(Duration.class) .addContent(".parse(\"") .addContent(defaultValue) @@ -168,6 +240,19 @@ Consumer> toDefaultValue(String defaultValue) { .addContent(defaultValue) .addContent("\".toCharArray()"); } + if (Types.PATH.equals(typeName)) { + return content -> content.addContent(Paths.class) + .addContent(".get(\"") + .addContent(defaultValue) + .addContent("\")"); + } + if (Types.URI.equals(typeName)) { + CodegenValidator.validateUri(enclosingType, annotatedMethod, OPTION_DEFAULT, "value", defaultValue); + return content -> content.addContent(URI.class) + .addContent(".create(\"") + .addContent(defaultValue) + .addContent("\")"); + } if (typeName.primitive()) { if (typeName.fqName().equals("char")) { return content -> content.addContent("'") @@ -488,6 +573,30 @@ protected void declaredSetter(InnerClass.Builder classBuilder, classBuilder.addMethod(builder); } + protected TypeName toPrimitive(TypeName typeName) { + return Optional.ofNullable(BOXED_TO_PRIMITIVE.get(typeName)) + .orElse(typeName); + } + + private static void checkTypeArgsSizeAndTypes(TypedElementInfo annotatedMethod, + TypeName returnType, + TypeName collectionType, + int expectedTypeArgs) { + List typeNames = returnType.typeArguments(); + if (typeNames.size() != expectedTypeArgs) { + throw new CodegenException("Property of type " + collectionType.fqName() + " must have " + expectedTypeArgs + + " type arguments defined", + annotatedMethod.originatingElementValue()); + } + for (TypeName typeName : typeNames) { + if (typeName.wildcard()) { + throw new CodegenException("Property of type " + returnType.resolvedName() + " is not supported for builder," + + " as wildcards cannot be handled correctly in setters", + annotatedMethod.originatingElementValue()); + } + } + } + private T singleDefault(List defaultValues) { if (defaultValues.isEmpty()) { throw new IllegalArgumentException("Default values configured for " + name() + " are empty, one value is expected."); @@ -614,8 +723,13 @@ private void factorySetter(InnerClass.Builder classBuilder, static class OneTypeHandler extends TypeHandler { private final TypeName actualType; - OneTypeHandler(String name, String getterName, String setterName, TypeName declaredType) { - super(name, getterName, setterName, declaredType); + OneTypeHandler(TypeName enclosingType, + TypedElementInfo annotatedMethod, + String name, + String getterName, + String setterName, + TypeName declaredType) { + super(enclosingType, annotatedMethod, name, getterName, setterName, declaredType); if (declaredType.typeArguments().isEmpty()) { this.actualType = TypeNames.STRING; diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java index 5d101949f5a..31ebdb6acca 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java @@ -50,6 +50,7 @@ import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; import static io.helidon.builder.codegen.Types.COMMON_CONFIG; import static io.helidon.codegen.CodegenUtil.capitalize; @@ -94,14 +95,16 @@ abstract class TypeHandlerCollection extends TypeHandler.OneTypeHandler { private final String collector; private final Optional configMapper; - TypeHandlerCollection(String name, + TypeHandlerCollection(TypeName blueprintType, + TypedElementInfo annotatedMethod, + String name, String getterName, String setterName, TypeName declaredType, TypeName collectionType, String collector, Optional configMapper) { - super(name, getterName, setterName, declaredType); + super(blueprintType, annotatedMethod, name, getterName, setterName, declaredType); this.collectionType = collectionType; this.collectionImplType = collectionImplType(collectionType); this.collector = collector; @@ -236,8 +239,13 @@ String generateMapListFromConfig(FactoryMethods factoryMethods) { @Override TypeName argumentTypeName() { + TypeName type = actualType(); + if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive() || type.array()) { + return declaredType(); + } + return TypeName.builder(collectionType) - .addTypeArgument(toWildcard(actualType())) + .addTypeArgument(toWildcard(type)) .build(); } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java index 7a551950c99..3f6273d9d9b 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java @@ -21,13 +21,16 @@ import io.helidon.codegen.CodegenUtil; import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; import static io.helidon.common.types.TypeNames.LIST; class TypeHandlerList extends TypeHandlerCollection { - TypeHandlerList(String name, String getterName, String setterName, TypeName declaredType) { - super(name, getterName, setterName, declaredType, LIST, "toList()", Optional.empty()); + TypeHandlerList(TypeName blueprintType, + TypedElementInfo annotatedMethod, + String name, String getterName, String setterName, TypeName declaredType) { + super(blueprintType, annotatedMethod, name, getterName, setterName, declaredType, LIST, "toList()", Optional.empty()); } static String isMutatedField(String propertyName) { diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java index d4502a521fd..8914159cb96 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java @@ -30,6 +30,7 @@ import io.helidon.codegen.classmodel.TypeArgument; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; import static io.helidon.codegen.CodegenUtil.capitalize; import static io.helidon.common.types.TypeNames.LIST; @@ -43,8 +44,10 @@ class TypeHandlerMap extends TypeHandler { private final TypeName implTypeName; private final boolean sameGeneric; - TypeHandlerMap(String name, String getterName, String setterName, TypeName declaredType, boolean sameGeneric) { - super(name, getterName, setterName, declaredType); + TypeHandlerMap(TypeName blueprintType, + TypedElementInfo annotatedMethod, + String name, String getterName, String setterName, TypeName declaredType, boolean sameGeneric) { + super(blueprintType, annotatedMethod, name, getterName, setterName, declaredType); this.sameGeneric = sameGeneric; this.implTypeName = collectionImplType(MAP); @@ -131,9 +134,18 @@ void generateFromConfig(Method.Builder method, @Override TypeName argumentTypeName() { + TypeName firstType = declaredType().typeArguments().get(0); + if (!(TypeNames.STRING.equals(firstType) || toPrimitive(firstType).primitive() || firstType.array())) { + firstType = toWildcard(firstType); + } + TypeName secondType = declaredType().typeArguments().get(1); + if (!(TypeNames.STRING.equals(secondType) || toPrimitive(secondType).primitive() || secondType.array())) { + secondType = toWildcard(secondType); + } + return TypeName.builder(MAP) - .addTypeArgument(toWildcard(declaredType().typeArguments().get(0))) - .addTypeArgument(toWildcard(declaredType().typeArguments().get(1))) + .addTypeArgument(firstType) + .addTypeArgument(secondType) .build(); } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java index c44ec497d68..ecb5a1dca30 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java @@ -17,7 +17,6 @@ package io.helidon.builder.codegen; import java.util.Iterator; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -27,48 +26,22 @@ import io.helidon.codegen.classmodel.Javadoc; import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; import static io.helidon.builder.codegen.Types.CHAR_ARRAY; import static io.helidon.codegen.CodegenUtil.capitalize; -import static io.helidon.common.types.TypeNames.BOXED_BOOLEAN; -import static io.helidon.common.types.TypeNames.BOXED_BYTE; -import static io.helidon.common.types.TypeNames.BOXED_CHAR; -import static io.helidon.common.types.TypeNames.BOXED_DOUBLE; -import static io.helidon.common.types.TypeNames.BOXED_FLOAT; -import static io.helidon.common.types.TypeNames.BOXED_INT; -import static io.helidon.common.types.TypeNames.BOXED_LONG; -import static io.helidon.common.types.TypeNames.BOXED_SHORT; -import static io.helidon.common.types.TypeNames.BOXED_VOID; import static io.helidon.common.types.TypeNames.OPTIONAL; -import static io.helidon.common.types.TypeNames.PRIMITIVE_BOOLEAN; -import static io.helidon.common.types.TypeNames.PRIMITIVE_BYTE; -import static io.helidon.common.types.TypeNames.PRIMITIVE_CHAR; -import static io.helidon.common.types.TypeNames.PRIMITIVE_DOUBLE; -import static io.helidon.common.types.TypeNames.PRIMITIVE_FLOAT; -import static io.helidon.common.types.TypeNames.PRIMITIVE_INT; -import static io.helidon.common.types.TypeNames.PRIMITIVE_LONG; -import static io.helidon.common.types.TypeNames.PRIMITIVE_SHORT; -import static io.helidon.common.types.TypeNames.PRIMITIVE_VOID; // declaration in builder is always non-generic, so no need to modify default values class TypeHandlerOptional extends TypeHandler.OneTypeHandler { - private static final Map BOXED_TO_PRIMITIVE = Map.of( - BOXED_BOOLEAN, PRIMITIVE_BOOLEAN, - BOXED_BYTE, PRIMITIVE_BYTE, - BOXED_SHORT, PRIMITIVE_SHORT, - BOXED_INT, PRIMITIVE_INT, - BOXED_LONG, PRIMITIVE_LONG, - BOXED_CHAR, PRIMITIVE_CHAR, - BOXED_FLOAT, PRIMITIVE_FLOAT, - BOXED_DOUBLE, PRIMITIVE_DOUBLE, - BOXED_VOID, PRIMITIVE_VOID - ); - - TypeHandlerOptional(String name, String getterName, String setterName, TypeName declaredType) { - super(name, getterName, setterName, declaredType); + TypeHandlerOptional(TypeName blueprintType, + TypedElementInfo annotatedMethod, + String name, String getterName, String setterName, TypeName declaredType) { + super(blueprintType, annotatedMethod, name, getterName, setterName, declaredType); } @Override @@ -95,14 +68,12 @@ Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilde @Override TypeName argumentTypeName() { TypeName type = actualType(); - if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive()) { - return TypeName.builder(OPTIONAL) - .addTypeArgument(type) - .build(); + if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive() || type.array()) { + return declaredType(); } return TypeName.builder(OPTIONAL) - .addTypeArgument(toWildcard(actualType())) + .addTypeArgument(toWildcard(type)) .build(); } @@ -235,8 +206,14 @@ void setters(InnerClass.Builder classBuilder, private void declaredSetter(InnerClass.Builder classBuilder, TypeName returnType, Javadoc blueprintJavadoc) { + boolean generic = !actualType().typeArguments().isEmpty(); // declared setter - optional is package local, field is never optional in builder classBuilder.addMethod(builder -> builder.name(setterName()) + .update(it -> { + if (generic) { + it.addAnnotation(Annotation.create(SuppressWarnings.class, "unchecked")); + } + }) .accessModifier(AccessModifier.PACKAGE_PRIVATE) .description(blueprintJavadoc.content()) .returnType(returnType, "updated builder instance") @@ -280,9 +257,4 @@ private String optionalSuffix(TypeName typeName) { } return ""; } - - private TypeName toPrimitive(TypeName typeName) { - return Optional.ofNullable(BOXED_TO_PRIMITIVE.get(typeName)) - .orElse(typeName); - } } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java index 1e146d5a879..20a68b5a7bf 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java @@ -19,13 +19,18 @@ import java.util.Optional; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; import static io.helidon.common.types.TypeNames.SET; class TypeHandlerSet extends TypeHandlerCollection { - TypeHandlerSet(String name, String getterName, String setterName, TypeName declaredType) { - super(name, + TypeHandlerSet(TypeName blueprintType, + TypedElementInfo annotatedMethod, + String name, String getterName, String setterName, TypeName declaredType) { + super(blueprintType, + annotatedMethod, + name, getterName, setterName, declaredType, diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java index a58aba704a5..0abfd028e27 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java @@ -25,14 +25,17 @@ import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; import static io.helidon.builder.codegen.Types.CHAR_ARRAY; import static io.helidon.common.types.TypeNames.SUPPLIER; class TypeHandlerSupplier extends TypeHandler.OneTypeHandler { - TypeHandlerSupplier(String name, String getterName, String setterName, TypeName declaredType) { - super(name, getterName, setterName, declaredType); + TypeHandlerSupplier(TypeName blueprintType, + TypedElementInfo annotatedMethod, + String name, String getterName, String setterName, TypeName declaredType) { + super(blueprintType, annotatedMethod, name, getterName, setterName, declaredType); } @Override @@ -52,8 +55,13 @@ Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilde @Override TypeName argumentTypeName() { + TypeName type = actualType(); + if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive() || type.array()) { + return declaredType(); + } + return TypeName.builder(SUPPLIER) - .addTypeArgument(toWildcard(actualType())) + .addTypeArgument(toWildcard(type)) .build(); } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java index 8a274abb2cb..2b7b56131ca 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java @@ -16,6 +16,7 @@ package io.helidon.builder.codegen; +import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -31,6 +32,8 @@ final class Types { static final TypeName ARRAY_LIST = TypeName.create(ArrayList.class); static final TypeName LINKED_HASH_SET = TypeName.create(LinkedHashSet.class); static final TypeName CHAR_ARRAY = TypeName.create(char[].class); + static final TypeName PATH = TypeName.create(Path.class); + static final TypeName URI = TypeName.create(java.net.URI.class); static final TypeName SERVICE_REGISTRY = TypeName.create("io.helidon.service.registry.ServiceRegistry"); static final TypeName GLOBAL_SERVICE_REGISTRY = TypeName.create("io.helidon.service.registry.GlobalServiceRegistry"); static final TypeName GENERATED_SERVICE = TypeName.create("io.helidon.service.registry.GeneratedService"); @@ -43,6 +46,7 @@ final class Types { static final TypeName PROTOTYPE_ANNOTATED = TypeName.create("io.helidon.builder.api.Prototype.Annotated"); static final TypeName PROTOTYPE_FACTORY = TypeName.create("io.helidon.builder.api.Prototype.Factory"); static final TypeName PROTOTYPE_CONFIGURED = TypeName.create("io.helidon.builder.api.Prototype.Configured"); + static final TypeName PROTOTYPE_PROVIDES = TypeName.create("io.helidon.builder.api.Prototype.Provides"); static final TypeName PROTOTYPE_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.Builder"); static final TypeName PROTOTYPE_CONFIGURED_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.ConfiguredBuilder"); static final TypeName PROTOTYPE_CUSTOM_METHODS = TypeName.create("io.helidon.builder.api.Prototype.CustomMethods"); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java index f0aa71335cb..f9ff370e4a1 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java @@ -23,6 +23,7 @@ import io.helidon.codegen.ElementInfoPredicates; import io.helidon.common.Errors; import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; @@ -41,7 +42,7 @@ private static void validateImplements(Errors.Collector errors, if (validatedType.interfaceTypeInfo() .stream() .noneMatch(it -> it.typeName().equals(implementedInterface))) { - errors.fatal(message); + errors.fatal(validatedType.typeName(), message); } } @@ -70,7 +71,7 @@ private static void validateFactoryMethod(Errors.Collector errors, }) .findFirst() .isEmpty()) { - errors.fatal(validatedType.typeName().fqName(), message); + errors.fatal(validatedType.typeName(), message); } } @@ -166,7 +167,21 @@ static class ValidateBlueprint extends ValidationTask { public void validate(Errors.Collector errors) { // must be package local if (blueprint.accessModifier() == AccessModifier.PUBLIC) { - errors.fatal(blueprint.typeName().fqName() + " is defined as public, it must be package local"); + errors.fatal(blueprint.typeName(), blueprint.typeName().fqName() + + " is defined as public, it must be package local"); + } + + // if configured & provides, must have config key + if (blueprint.hasAnnotation(Types.PROTOTYPE_CONFIGURED) + && blueprint.hasAnnotation(Types.PROTOTYPE_PROVIDES)) { + Annotation configured = blueprint.annotation(Types.PROTOTYPE_CONFIGURED); + String value = configured.stringValue().orElse(""); + if (value.isEmpty()) { + // we have a @Configured and @Provides - this should have a configuration key! + errors.fatal(blueprint.typeName(), blueprint.typeName().fqName() + + " is marked as @Configured and @Provides, yet it does not" + + " define a configuration key"); + } } } @@ -276,8 +291,9 @@ void validate(Errors.Collector errors) { if (typeInfo.findAnnotation(annotation) .stream() .noneMatch(it -> it.value().map(expectedValue::equals).orElse(false))) { - errors.fatal("Type " + typeInfo.typeName() - .fqName() + " must be annotated with " + annotation.fqName() + "(" + expectedValue + ")"); + errors.fatal(typeInfo.typeName(), + "Type " + typeInfo.typeName().fqName() + + " must be annotated with " + annotation.fqName() + "(" + expectedValue + ")"); } } } diff --git a/builder/pom.xml b/builder/pom.xml index 3d1322a6a87..6362a060faa 100644 --- a/builder/pom.xml +++ b/builder/pom.xml @@ -25,8 +25,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 @@ -40,6 +39,14 @@ api codegen processor - tests + + + + tests + + tests + + + diff --git a/builder/processor/pom.xml b/builder/processor/pom.xml index 29675509222..9bfc0949cc1 100644 --- a/builder/processor/pom.xml +++ b/builder/processor/pom.xml @@ -23,8 +23,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java index 2d744cc8b85..f55d1fbba88 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,13 @@ static TypeName toWildcard(TypeName typeName) { if (typeName.wildcard()) { return typeName; } - return TypeName.builder(typeName).wildcard(true).build(); + if (typeName.equals(TypeNames.STRING)) { + return typeName; + } + if (typeName.typeArguments().isEmpty()) { + return TypeName.builder(typeName).wildcard(true).build(); + } + return typeName; } protected static TypeName collectionImplType(TypeName typeName) { diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java index b424700b30f..813e7071927 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -215,9 +215,15 @@ String generateMapListFromConfig(FactoryMethods factoryMethods) { @Override TypeName argumentTypeName() { - return TypeName.builder(collectionType) - .addTypeArgument(toWildcard(actualType())) - .build(); + if (actualType().equals(TypeNames.OBJECT)) { + return TypeName.builder(collectionType) + .addTypeArgument(TypeName.builder() + .from(TypeNames.OBJECT) + .wildcard(true) + .build()) + .build(); + } + return declaredType(); } @Override diff --git a/builder/tests/builder/pom.xml b/builder/tests/builder/pom.xml index 29e3b09dac8..e60957cd550 100644 --- a/builder/tests/builder/pom.xml +++ b/builder/tests/builder/pom.xml @@ -23,8 +23,7 @@ io.helidon.builder.tests helidon-builder-tests-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GenericsBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GenericsBlueprint.java new file mode 100644 index 00000000000..a2af630974c --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GenericsBlueprint.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.test.testsubjects; + +import java.io.Serializable; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +// test all the funny generics +@Prototype.Blueprint +interface GenericsBlueprint { + @Option.Singular + Set tValues(); + + @Option.Singular + Set xValues(); + + @Option.Singular + Map mappedValues(); + + Optional> complicatedValue(); +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/tostring/SecretBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/tostring/SecretBlueprint.java new file mode 100644 index 00000000000..2328d1b7c4b --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/tostring/SecretBlueprint.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.test.testsubjects.tostring; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +interface SecretBlueprint { + String name(); + @Option.Confidential + String secret(); +} diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/GenericTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/GenericTest.java new file mode 100644 index 00000000000..56302b9f099 --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/GenericTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.test; + +import java.io.Serializable; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.builder.test.testsubjects.Generics; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +class GenericTest { + + @Test + void genericsTest() { + Generics.Builder builder = Generics.builder(); + builder.addTValue(new ImplOfT("firstTValue")); + builder.addTValue(new ImplOfT("secondTValue")); + builder.addXValue(new ImplOfT("firstXValue")); + builder.addXValue(new ImplOfT("secondXValue")); + builder.putMappedValue(new ImplOfT("key"), new ImplOfT("value")); + builder.putMappedValue(new ImplOfT("key2"), new ImplOfT("value2")); + builder.complicatedValue(new Supply()); + + Generics generics = builder.build(); + + assertThat(generics.tValues(), hasItems(new ImplOfT("firstTValue"), new ImplOfT("secondTValue"))); + assertThat(generics.xValues(), hasItems(new ImplOfT("firstXValue"), new ImplOfT("secondXValue"))); + assertThat(generics.complicatedValue(), not(Optional.empty())); + assertThat(generics.complicatedValue().get().get(), is(new ImplOfT("supplied"))); + assertThat(generics.mappedValues().size(), is(2)); + } + + private static class Supply implements Supplier { + @Override + public ImplOfT get() { + return new ImplOfT("supplied"); + } + } + private static class ImplOfT implements CharSequence, Serializable { + private final String delegate; + + private ImplOfT(String delegate) { + this.delegate = delegate; + } + + @Override + public int length() { + return delegate.length(); + } + + @Override + public char charAt(int index) { + return delegate.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return delegate.subSequence(start, end); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ImplOfT implOfT)) { + return false; + } + return Objects.equals(delegate, implOfT.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } + } +} diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/ToStringTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/ToStringTest.java new file mode 100644 index 00000000000..49852db85dc --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/ToStringTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.test; + +import io.helidon.builder.test.testsubjects.tostring.Secret; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +class ToStringTest { + @Test + void testToStringWithConfidential() { + var builder = Secret.builder() + .name("public-name") + .secret("secret-value"); + var secret = builder.build(); + + assertThat(builder.toString(), containsString("public-name")); + assertThat(builder.toString(), not(containsString("secret-value"))); + + assertThat(secret.toString(), containsString("public-name")); + assertThat(secret.toString(), not(containsString("secret-value"))); + } +} diff --git a/builder/tests/codegen/pom.xml b/builder/tests/codegen/pom.xml index a53fee91f63..56aae2c3df5 100644 --- a/builder/tests/codegen/pom.xml +++ b/builder/tests/codegen/pom.xml @@ -23,8 +23,7 @@ io.helidon.builder.tests helidon-builder-tests-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java b/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java index f5c1d35db9c..a2cc8559586 100644 --- a/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java +++ b/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java @@ -18,6 +18,8 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.net.URI; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -84,6 +86,8 @@ void testTypes() { checkField(toCheck, checked, fields, "ARRAY_LIST", ArrayList.class); checkField(toCheck, checked, fields, "LINKED_HASH_SET", LinkedHashSet.class); checkField(toCheck, checked, fields, "CHAR_ARRAY", char[].class); + checkField(toCheck, checked, fields, "PATH", Path.class); + checkField(toCheck, checked, fields, "URI", URI.class); checkField(toCheck, checked, fields, "SERVICE_REGISTRY", ServiceRegistry.class); checkField(toCheck, checked, fields, "GLOBAL_SERVICE_REGISTRY", GlobalServiceRegistry.class); checkField(toCheck, checked, fields, "GENERATED_SERVICE", GeneratedService.class); @@ -94,6 +98,7 @@ void testTypes() { checkField(toCheck, checked, fields, "PROTOTYPE_ANNOTATED", Prototype.Annotated.class); checkField(toCheck, checked, fields, "PROTOTYPE_FACTORY", Prototype.Factory.class); checkField(toCheck, checked, fields, "PROTOTYPE_CONFIGURED", Prototype.Configured.class); + checkField(toCheck, checked, fields, "PROTOTYPE_PROVIDES", Prototype.Provides.class); checkField(toCheck, checked, fields, "PROTOTYPE_BUILDER", Prototype.Builder.class); checkField(toCheck, checked, fields, "PROTOTYPE_CONFIGURED_BUILDER", Prototype.ConfiguredBuilder.class); checkField(toCheck, checked, fields, "PROTOTYPE_CUSTOM_METHODS", Prototype.CustomMethods.class); diff --git a/builder/tests/common-types/pom.xml b/builder/tests/common-types/pom.xml index 1185096b64a..f5613f14b5b 100644 --- a/builder/tests/common-types/pom.xml +++ b/builder/tests/common-types/pom.xml @@ -23,8 +23,7 @@ io.helidon.builder.tests helidon-builder-tests-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java index 07f13ac5c29..3aab5620bdf 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java @@ -43,6 +43,8 @@ public interface Annotated { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return list of all meta annotations of this element */ @@ -85,11 +87,10 @@ default Optional findAnnotation(TypeName annotationType) { * @see #findAnnotation(TypeName) */ default Annotation annotation(TypeName annotationType) { - return findAnnotation(annotationType).orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " " - + "is not present. Guard " - + "with hasAnnotation(), or " - + "use findAnnotation() " - + "instead")); + return findAnnotation(annotationType) + .orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " is not present. " + + "Guard with hasAnnotation(), " + + "or use findAnnotation() instead")); } /** diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java index 3017fdd6526..b51fb659b6d 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java @@ -79,6 +79,15 @@ interface AnnotationBlueprint { @Option.Singular Map values(); + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return list of all annotations declared on the annotation type, or inherited from them + */ + @Option.Redundant + @Option.Singular + List metaAnnotations(); + /** * The value property. * @@ -653,4 +662,19 @@ default > Optional> enumValues(String property, Class< return AnnotationSupport.asEnums(typeName(), values(), property, type); } + /** + * Check if {@link io.helidon.common.types.Annotation#metaAnnotations()} contains an annotation of the provided type. + *

+ * Note: we ignore {@link java.lang.annotation.Target}, {@link java.lang.annotation.Inherited}, + * {@link java.lang.annotation.Documented}, and {@link java.lang.annotation.Retention}. + * + * @param annotationType type of annotation + * @return {@code true} if the annotation is declared on this annotation, or is inherited from a declared annotation + */ + default boolean hasMetaAnnotation(TypeName annotationType) { + return metaAnnotations() + .stream() + .map(Annotation::typeName) + .anyMatch(annotationType::equals); + } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java index 0101b393de7..b21a8a1a917 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -532,6 +532,14 @@ private static String asString(TypeName typeName, String property, Object value) return str; } + if (value instanceof TypeName tn) { + return tn.fqName(); + } + + if (value instanceof EnumValue ev) { + return ev.name(); + } + if (value instanceof List) { throw new IllegalArgumentException(typeName.fqName() + " property " + property + " is a list, cannot be converted to String"); @@ -619,22 +627,30 @@ private static Class asClass(TypeName typeName, String property, Object value if (value instanceof Class theClass) { return theClass; } - if (value instanceof String str) { - try { - return Class.forName(str); - } catch (ClassNotFoundException e) { + + String className = switch (value) { + case TypeName tn -> tn.name(); + case String str -> str; + default -> { throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type String and value \"" + str + "\"" + + " of type " + value.getClass().getName() + " cannot be converted to Class"); } - } + }; - throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type " + value.getClass().getName() - + " cannot be converted to Class"); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(typeName.fqName() + " property " + property + + " of type String and value \"" + className + "\"" + + " cannot be converted to Class"); + } } private static TypeName asTypeName(TypeName typeName, String property, Object value) { + if (value instanceof TypeName tn) { + return tn; + } if (value instanceof Class theClass) { return TypeName.create(theClass); } @@ -664,6 +680,15 @@ private static > T asEnum(TypeName typeName, String property, if (value instanceof String str) { return Enum.valueOf(type, str); } + if (value instanceof EnumValue enumValue) { + if (enumValue.type().equals(TypeName.create(type))) { + return Enum.valueOf(type, enumValue.name()); + } + + throw new IllegalStateException("Property " + property + " is of enum type for enum " + + enumValue.type().fqName() + ", yet you requested " + + type.getName()); + } throw new IllegalArgumentException(typeName.fqName() + " property " + property + " of type " + value.getClass().getName() diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java new file mode 100644 index 00000000000..a167313516d --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; + +/** + * Signature of a {@link io.helidon.common.types.TypedElementInfo}. + *

+ * The {@link io.helidon.common.types.TypedElementInfo#signature()} is intended to compare + * fields, methods, and constructors across type hierarchy - for example when looking for a method + * that we override. + *

+ * The following information is used for equals and hash-code: + *

    + *
  • Field: field name
  • + *
  • Constructor: parameter types
  • + *
  • Method: method name, parameter types
  • + *
  • Parameter: this signature is not useful, as we cannot depend on parameter names
  • + *
+ * + * The signature has well-defined {@code hashCode} and {@code equals} methods, + * so it can be safely used as a key in a {@link java.util.Map}. + *

+ * This interface is sealed, an instance can only be obtained + * from {@link io.helidon.common.types.TypedElementInfo#signature()}. + * + * @see #text() + */ +public sealed interface ElementSignature permits ElementSignatures.FieldSignature, + ElementSignatures.MethodSignature, + ElementSignatures.ParameterSignature, + ElementSignatures.NoSignature { + /** + * Type of the element. Resolves as follows: + *

    + *
  • Field: type of the field
  • + *
  • Constructor: void
  • + *
  • Method: method return type
  • + *
  • Parameter: parameter type
  • + *
+ * + * @return type of this element, never used for equals or hashCode + */ + TypeName type(); + + /** + * Name of the element. For constructor, this always returns {@code }, + * for parameters, this method may return the real parameter name or an index + * parameter name depending on the source of the information (during annotation processing, + * this would be the actual parameter name, when classpath scanning, this would be something like + * {@code param0}. + * + * @return name of this element + */ + String name(); + + /** + * Types of parameters if this represents a method or a constructor, + * empty {@link java.util.List} otherwise. + * + * @return parameter types + */ + List parameterTypes(); + + /** + * A text representation of this signature. + * + *
    + *
  • Field: field name (such as {@code myNiceField}
  • + *
  • Constructor: comma separated parameter types (no generics) in parentheses (such as + * {@code (java.lang.String,java.util.List)})
  • + *
  • Method: method name, parameter types (no generics) in parentheses (such as + * {@code methodName(java.lang.String,java.util.List)}
  • + *
  • Parameter: parameter name (such as {@code myParameter} or {@code param0} - not very useful, as parameter names + * are not carried over to compiled code in Java
  • + *
+ * + * @return text representation + */ + String text(); +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java new file mode 100644 index 00000000000..4f5be563f29 --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class ElementSignatures { + private ElementSignatures() { + } + + static ElementSignature createNone() { + return new NoSignature(); + } + + static ElementSignature createField(TypeName type, + String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new FieldSignature(type, name); + } + + static ElementSignature createConstructor(List parameters) { + Objects.requireNonNull(parameters); + return new MethodSignature(TypeNames.PRIMITIVE_VOID, + "", + parameters); + } + + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + Objects.requireNonNull(returnType); + Objects.requireNonNull(name); + Objects.requireNonNull(parameters); + return new MethodSignature(returnType, + name, + parameters); + } + + static ElementSignature createParameter(TypeName type, String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new ParameterSignature(type, name); + } + + static final class FieldSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private FieldSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldSignature that)) { + return false; + } + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class MethodSignature implements ElementSignature { + private final TypeName type; + private final String name; + private final List parameters; + private final String text; + private final boolean constructor; + + private MethodSignature(TypeName type, + String name, + List parameters) { + this.type = type; + this.name = name; + this.parameters = parameters; + if (name.equals("")) { + this.constructor = true; + this.text = parameterTypesSection(parameters, ",", TypeName::fqName); + } else { + this.constructor = false; + this.text = name + parameterTypesSection(parameters, ",", TypeName::fqName); + } + + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return parameters; + } + + @Override + public String text() { + return text; + } + + @Override + public String toString() { + if (constructor) { + return text; + } else { + return type.resolvedName() + " " + name + parameterTypesSection(parameters, + ", ", + TypeName::resolvedName); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodSignature that)) { + return false; + } + return Objects.equals(name, that.name) && Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters); + } + } + + static final class ParameterSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private ParameterSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParameterSignature that)) { + return false; + } + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class NoSignature implements ElementSignature { + @Override + public TypeName type() { + return TypeNames.PRIMITIVE_VOID; + } + + @Override + public String name() { + return ""; + } + + @Override + public String text() { + return ""; + } + + @Override + public String toString() { + return text(); + } + + @Override + public List parameterTypes() { + return List.of(); + } + } + + private static String parameterTypesSection(List parameters, + String delimiter, + Function typeMapper) { + return parameters.stream() + .map(typeMapper) + .collect(Collectors.joining(delimiter, "(", ")")); + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java new file mode 100644 index 00000000000..3a405d354f2 --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Objects; + +/** + * When creating an {@link io.helidon.common.types.Annotation}, we may need to create an enum value + * without access to the enumeration. + *

+ * In such a case, you can use this type when calling {@link io.helidon.common.types.Annotation.Builder#putValue(String, Object)} + */ +public interface EnumValue { + /** + * Create a new enum value, when the enum is not available on classpath. + * + * @param enumType type of the enumeration + * @param enumName value of the enumeration + * @return enum value + */ + static EnumValue create(TypeName enumType, String enumName) { + Objects.requireNonNull(enumType); + Objects.requireNonNull(enumName); + return new EnumValueImpl(enumType, enumName); + } + + /** + * Create a new enum value. + * + * @param type enum type + * @param value enum value constant + * @return new enum value + * @param type of the enum + */ + static > EnumValue create(Class type, T value) { + Objects.requireNonNull(type); + Objects.requireNonNull(value); + + return new EnumValueImpl(TypeName.create(type), value.name()); + } + + /** + * Type of the enumeration. + * + * @return type name of the enumeration + */ + TypeName type(); + + /** + * The enum value. + * + * @return enum value + */ + String name(); +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java new file mode 100644 index 00000000000..f2205b2452e --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Objects; + +final class EnumValueImpl implements EnumValue { + private final TypeName type; + private final String name; + + EnumValueImpl(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EnumValue enumValue)) { + return false; + } + return Objects.equals(type, enumValue.type()) && Objects.equals(name, enumValue.name()); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + + @Override + public String toString() { + return type.fqName() + "." + name; + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java index 87a1e75e4db..7d36ffc7ece 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,23 @@ public enum Modifier { /** * The {@code final} modifier. */ - FINAL("final"); + FINAL("final"), + /** + * The {@code transient} modifier. + */ + TRANSIENT("transient"), + /** + * The {@code volatile} modifier. + */ + VOLATILE("volatile"), + /** + * The {@code synchronized} modifier. + */ + SYNCHRONIZED("synchronized"), + /** + * The {@code native} modifier. + */ + NATIVE("native"); private final String modifierName; diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java new file mode 100644 index 00000000000..e962800d60a --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.lang.reflect.Type; + +/** + * A wrapper for {@link io.helidon.common.types.TypeName} that uses the resolved name for equals and hashCode. + * This allows us to collect interfaces including type arguments. + * + * @see TypeName#resolvedName() + */ +public interface ResolvedType { + /** + * Create a type name from a type (such as class). + * + * @param type the type + * @return type name for the provided type + */ + static ResolvedType create(Type type) { + return new ResolvedTypeImpl(TypeName.create(type)); + } + + /** + * Creates a type name from a fully qualified class name. + * + * @param typeName the FQN of the class type + * @return the TypeName for the provided type name + */ + static ResolvedType create(String typeName) { + return new ResolvedTypeImpl(TypeName.create(typeName)); + } + + /** + * Create a type name from a type name. + * + * @param typeName the type + * @return type name for the provided type + */ + static ResolvedType create(TypeName typeName) { + if (typeName instanceof ResolvedType rt) { + return rt; + } + return new ResolvedTypeImpl(typeName); + } + + /** + * Provides the underlying type name that backs this resolved type. + * + * @return the type name this resolved type represents + */ + TypeName type(); +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java new file mode 100644 index 00000000000..24c73cd5fc6 --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +class ResolvedTypeImpl implements ResolvedType, Comparable { + private final TypeName typeName; + private final String resolvedName; + private final boolean noTypes; + + ResolvedTypeImpl(TypeName typeName) { + this.typeName = typeName; + this.resolvedName = typeName.resolvedName(); + this.noTypes = typeName.typeArguments().isEmpty(); + } + + @Override + public TypeName type() { + return typeName; + } + + @Override + public int hashCode() { + return noTypes ? typeName.hashCode() : resolvedName.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ResolvedType other)) { + return false; + } + if (other instanceof ResolvedTypeImpl rti) { + return resolvedName.equals(rti.resolvedName); + } + return other.type().resolvedName().equals(resolvedName); + } + + @Override + public int compareTo(ResolvedType o) { + int diff = resolvedName.compareTo(o.type().resolvedName()); + if (diff != 0) { + // different name + return diff; + } + diff = Boolean.compare(typeName.primitive(), o.type().primitive()); + if (diff != 0) { + return diff; + } + return Boolean.compare(typeName.array(), o.type().array()); + } + + @Override + public String toString() { + return resolvedName; + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 4bb3da96300..e744bb97bae 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -16,10 +16,13 @@ package io.helidon.common.types; +import java.util.ArrayDeque; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import io.helidon.builder.api.Option; @@ -32,12 +35,34 @@ interface TypeInfoBlueprint extends Annotated { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @return the type name */ @Option.Required TypeName typeName(); + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *

    + *
  • {@link TypeName#packageName()}
  • + *
  • {@link io.helidon.common.types.TypeName#className()}
  • + *
  • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
  • + *
+ * + * @return raw type of this type info + */ + TypeName rawType(); + + /** + * The declared type name, including type parameters. + * + * @return type name with declared type parameters + */ + TypeName declaredType(); + /** * Description, such as javadoc, if available. * @@ -228,6 +253,54 @@ default Optional metaAnnotation(TypeName annotation, TypeName metaAn @Option.Redundant Optional originatingElement(); + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypeInfo#typeName()} if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code ClassInfo} when using classpath scanning. + * + * @return originating element, or the type of this type info + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::typeName); + } + + /** + * Checks if the current type implements, or extends the provided type. + * This method analyzes the whole dependency tree of the current type. + * + * @param typeName type of interface to check + * @return the super type info, or interface type info matching the provided type, with appropriate generic declarations + */ + default Optional findInHierarchy(TypeName typeName) { + if (typeName.equals(typeName())) { + return Optional.of((TypeInfo) this); + } + // scan super types + Optional superClass = superTypeInfo(); + if (superClass.isPresent() && !superClass.get().typeName().equals(TypeNames.OBJECT)) { + var superType = superClass.get(); + var foundInSuper = superType.findInHierarchy(typeName); + if (foundInSuper.isPresent()) { + return foundInSuper; + } + } + // nope, let's try interfaces + Queue interfaces = new ArrayDeque<>(interfaceTypeInfo()); + Set processed = new HashSet<>(); + + while (!interfaces.isEmpty()) { + TypeInfo type = interfaces.remove(); + // make sure we process each type only once + if (processed.add(type.typeName())) { + if (typeName.equals(type.typeName())) { + return Optional.of(type); + } + interfaces.addAll(type.interfaceTypeInfo()); + } + } + return Optional.empty(); + } + /** * Uses {@link io.helidon.common.types.TypeInfo#referencedModuleNames()} to determine if the module name is known for the * given type. diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java index a054c002053..952cd31cc0e 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,18 @@ public void decorate(TypeInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); + + // new methods, simplify for tests + if (target.rawType().isEmpty()) { + target.typeName() + .map(TypeName::genericTypeName) + .ifPresent(target::rawType); + } + if (target.declaredType().isEmpty()) { + // this may not be correct, but is correct for all types that do not have any declaration of generics + // so it simplifies a lot of use cases + target.rawType().ifPresent(target::declaredType); + } } } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java index 1d9d5fa7118..d712863b5b4 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java @@ -44,7 +44,7 @@ *
  • {@link #declaredName()} and {@link #resolvedName()}.
  • * */ -@Prototype.Blueprint +@Prototype.Blueprint(decorator = TypeNameSupport.Decorator.class) @Prototype.CustomMethods(TypeNameSupport.class) @Prototype.Implement("java.lang.Comparable") interface TypeNameBlueprint { @@ -137,11 +137,39 @@ default String classNameWithEnclosingNames() { * if {@link #typeArguments()} exist, this list MUST exist and have the same size and order (it maps the name to the type). * * @return type parameter names as declared on this type, or names that represent the {@link #typeArguments()} + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information */ @Option.Singular @Option.Redundant + @Deprecated(forRemoval = true, since = "4.2.0") List typeParameters(); + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of lower bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List lowerBounds(); + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of upper bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List upperBounds(); + /** * Indicates whether this type is a {@code java.util.List}. * diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java index e8a9d38809b..7c87d4eaced 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.LinkedList; @@ -23,9 +24,11 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.helidon.builder.api.Prototype; +import io.helidon.common.GenericType; final class TypeNameSupport { private static final TypeName PRIMITIVE_BOOLEAN = TypeName.create(boolean.class); @@ -141,59 +144,57 @@ static String fqName(TypeName instance) { @Prototype.PrototypeMethod @Prototype.Annotated("java.lang.Override") // defined on blueprint static String resolvedName(TypeName instance) { - String name = calcName(instance, "."); - boolean isObject = Object.class.getName().equals(name) || "?".equals(name); - StringBuilder nameBuilder = (isObject) - ? new StringBuilder(instance.wildcard() ? "?" : name) - : new StringBuilder(instance.wildcard() ? "? extends " + name : name); - - if (!instance.typeArguments().isEmpty()) { - nameBuilder.append("<"); - int i = 0; - for (TypeName param : instance.typeArguments()) { - if (i > 0) { - nameBuilder.append(", "); - } - nameBuilder.append(param.resolvedName()); - i++; - } - nameBuilder.append(">"); + if (instance.generic() || instance.wildcard()) { + return resolveGenericName(instance); } - - if (instance.array()) { - nameBuilder.append("[]"); - } - - return nameBuilder.toString(); + return resolveClassName(instance); } /** * Update builder from the provided type. * * @param builder builder to update - * @param type type to get information (package name, class name, primitive, array) + * @param type type to get information (package name, class name, primitive, array) */ @Prototype.BuilderMethod static void type(TypeName.BuilderBase builder, Type type) { Objects.requireNonNull(type); if (type instanceof Class classType) { - Class componentType = classType.isArray() ? classType.getComponentType() : classType; - builder.packageName(componentType.getPackageName()); - builder.className(componentType.getSimpleName()); - builder.primitive(componentType.isPrimitive()); - builder.array(classType.isArray()); - - Class enclosingClass = classType.getEnclosingClass(); - LinkedList enclosingTypes = new LinkedList<>(); - while (enclosingClass != null) { - enclosingTypes.addFirst(enclosingClass.getSimpleName()); - enclosingClass = enclosingClass.getEnclosingClass(); + updateFromClass(builder, classType); + return; + } + Type reflectGenericType = type; + + if (type instanceof GenericType gt) { + if (gt.isClass()) { + // simple case - just a class + updateFromClass(builder, gt.rawType()); + return; + } else { + // complex case - has generic type arguments + reflectGenericType = gt.type(); } - builder.enclosingNames(enclosingTypes); - } else { - // todo - throw new IllegalArgumentException("Currently we only support class as a parameter, but got: " + type); } + + // translate the generic type into type name + if (reflectGenericType instanceof ParameterizedType pt) { + Type raw = pt.getRawType(); + if (raw instanceof Class theClass) { + updateFromClass(builder, theClass); + } else { + throw new IllegalArgumentException("Raw type of a ParameterizedType is not a class: " + raw.getClass().getName() + + ", for " + pt.getTypeName()); + } + + Type[] actualTypeArguments = pt.getActualTypeArguments(); + for (Type actualTypeArgument : actualTypeArguments) { + builder.addTypeArgument(TypeName.create(actualTypeArgument)); + } + return; + } + + throw new IllegalArgumentException("We can only create a type from a class, GenericType, or a ParameterizedType," + + " but got: " + reflectGenericType.getClass().getName()); } /** @@ -309,6 +310,71 @@ static TypeName createFromGenericDeclaration(String genericAliasTypeName) { .build(); } + private static String resolveGenericName(TypeName instance) { + // ?, ? super Something; ? extends Something + String prefix = instance.wildcard() ? "?" : instance.className(); + if (instance.upperBounds().isEmpty() && instance.lowerBounds().isEmpty()) { + return prefix; + } + if (instance.lowerBounds().isEmpty()) { + return prefix + " extends " + instance.upperBounds() + .stream() + .map(it -> { + if (it.generic()) { + return it.wildcard() ? "?" : it.className(); + } + return it.resolvedName(); + }) + .collect(Collectors.joining(" & ")); + } + TypeName lowerBound = instance.lowerBounds().getFirst(); + if (lowerBound.generic()) { + return prefix + " super " + (lowerBound.wildcard() ? "?" : lowerBound.className()); + } + return prefix + " super " + lowerBound.resolvedName(); + + } + + private static String resolveClassName(TypeName instance) { + String name = calcName(instance, "."); + StringBuilder nameBuilder = new StringBuilder(name); + + if (!instance.typeArguments().isEmpty()) { + nameBuilder.append("<"); + int i = 0; + for (TypeName param : instance.typeArguments()) { + if (i > 0) { + nameBuilder.append(", "); + } + nameBuilder.append(param.resolvedName()); + i++; + } + nameBuilder.append(">"); + } + + if (instance.array()) { + nameBuilder.append("[]"); + } + + return nameBuilder.toString(); + } + + private static void updateFromClass(TypeName.BuilderBase builder, Class classType) { + Class componentType = classType.isArray() ? classType.getComponentType() : classType; + builder.packageName(componentType.getPackageName()); + builder.className(componentType.getSimpleName()); + builder.primitive(componentType.isPrimitive()); + builder.array(classType.isArray()); + + Class enclosingClass = classType.getEnclosingClass(); + LinkedList enclosingTypes = new LinkedList<>(); + while (enclosingClass != null) { + enclosingTypes.addFirst(enclosingClass.getSimpleName()); + enclosingClass = enclosingClass.getEnclosingClass(); + } + builder.enclosingNames(enclosingTypes); + } + private static String calcName(TypeName instance, String typeSeparator) { String className; if (instance.enclosingNames().isEmpty()) { @@ -320,4 +386,38 @@ private static String calcName(TypeName instance, String typeSeparator) { return (instance.primitive() || instance.packageName().isEmpty()) ? className : instance.packageName() + "." + className; } + + static class Decorator implements Prototype.BuilderDecorator> { + @Override + public void decorate(TypeName.BuilderBase target) { + fixWildcards(target); + } + + private void fixWildcards(TypeName.BuilderBase target) { + // handle wildcards correct + if (target.wildcard()) { + if (target.upperBounds().size() == 1 && target.lowerBounds().isEmpty()) { + // backward compatible for (? extends X) + TypeName upperBound = target.upperBounds().getFirst(); + target.className(upperBound.className()); + target.packageName(upperBound.packageName()); + target.enclosingNames(upperBound.enclosingNames()); + } + // wildcard set, if package + class name as well, set them as upper bounds + if (target.className().isPresent() + && !target.className().get().equals("?") + && target.upperBounds().isEmpty() + && target.lowerBounds().isEmpty()) { + TypeName upperBound = TypeName.builder() + .from(target) + .wildcard(false) + .build(); + if (!upperBound.equals(TypeNames.OBJECT)) { + target.addUpperBound(upperBound); + } + } + target.generic(true); + } + } + } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java index 62fe59da5a3..caf18dbf866 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java @@ -16,8 +16,10 @@ package io.helidon.common.types; +import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -73,10 +75,18 @@ public final class TypeNames { * Type name for {@link java.lang.annotation.Retention}. */ public static final TypeName RETENTION = TypeName.create(Retention.class); + /** + * Type name for {@link java.lang.annotation.Documented}. + */ + public static final TypeName DOCUMENTED = TypeName.create(Documented.class); /** * Type name for {@link java.lang.annotation.Inherited}. */ public static final TypeName INHERITED = TypeName.create(Inherited.class); + /** + * Type name for {@link java.lang.annotation.Target}. + */ + public static final TypeName TARGET = TypeName.create(Target.class); /* Primitive types and their boxed counterparts @@ -161,6 +171,10 @@ public final class TypeNames { * Type name of the type name. */ public static final TypeName TYPE_NAME = TypeName.create(TypeName.class); + /** + * Type name of the resolved type name. + */ + public static final TypeName RESOLVED_TYPE_NAME = TypeName.create(ResolvedType.class); /** * Type name of typed element info. */ diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java index b52ba88a4bd..3de312eb13c 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java @@ -167,4 +167,25 @@ interface TypedElementInfoBlueprint extends Annotated { */ @Option.Redundant Optional originatingElement(); + + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypedElementInfo#signature()} + * if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code MethodInfo} (and such) when using classpath scanning. + * + * @return originating element, or the signature of this element + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::signature); + } + + /** + * Signature of this element. + * + * @return signature of this element + * @see io.helidon.common.types.ElementSignature + */ + @Option.Access("") + ElementSignature signature(); } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java index 9edf23abd11..0cba7edaff8 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; @@ -60,10 +61,67 @@ static class BuilderDecorator implements Prototype.BuilderDecorator target) { -/* + backwardCompatibility(target); + constructorName(target); + signature(target); + } + + private void signature(TypedElementInfo.BuilderBase target) { + if (target.kind().isEmpty()) { + // this will fail when validating + target.signature(ElementSignatures.createNone()); + return; + } else { + target.signature(signature(target, target.kind().get())); + } + } + + private ElementSignature signature(TypedElementInfo.BuilderBase target, ElementKind elementKind) { + if (elementKind == ElementKind.CONSTRUCTOR) { + return ElementSignatures.createConstructor(toTypes(target.parameterArguments())); + } + // for everything else we need the type (it is required) + if (target.typeName().isEmpty() || target.elementName().isEmpty()) { + return ElementSignatures.createNone(); + } + + TypeName typeName = target.typeName().get(); + String name = target.elementName().get(); + + if (elementKind == ElementKind.FIELD + || elementKind == ElementKind.RECORD_COMPONENT + || elementKind == ElementKind.ENUM_CONSTANT) { + return ElementSignatures.createField(typeName, name); + } + if (elementKind == ElementKind.METHOD) { + return ElementSignatures.createMethod(typeName, name, toTypes(target.parameterArguments())); + } + if (elementKind == ElementKind.PARAMETER) { + return ElementSignatures.createParameter(typeName, name); + } + return ElementSignatures.createNone(); + } + + private List toTypes(List typedElementInfos) { + return typedElementInfos.stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()); + } + + private void constructorName(TypedElementInfo.BuilderBase target) { + Optional elementKind = target.kind(); + if (elementKind.isPresent()) { + if (elementKind.get() == ElementKind.CONSTRUCTOR) { + target.elementName(""); + } + } + } + + @SuppressWarnings("removal") + private void backwardCompatibility(TypedElementInfo.BuilderBase target) { + /* Backward compatibility for deprecated methods. */ if (target.kind().isEmpty() && target.elementTypeKind().isPresent()) { @@ -103,14 +161,6 @@ public void decorate(TypedElementInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); - - - Optional elementKind = target.kind(); - if (elementKind.isPresent()) { - if (elementKind.get() == ElementKind.CONSTRUCTOR) { - target.elementName(""); - } - } } } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java new file mode 100644 index 00000000000..7851acd401b --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Subset of Builder's SPI types that are useful for runtime. Used in the ConfigBean builder, etc., that require a minimal set of + * types present at runtime. + */ +package io.helidon.common.types; diff --git a/builder/tests/pom.xml b/builder/tests/pom.xml index 8772249f6dc..094e16c96ff 100644 --- a/builder/tests/pom.xml +++ b/builder/tests/pom.xml @@ -25,8 +25,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT 4.0.0 diff --git a/bundles/config/pom.xml b/bundles/config/pom.xml index 78f110fa2df..65e32af242e 100644 --- a/bundles/config/pom.xml +++ b/bundles/config/pom.xml @@ -22,7 +22,7 @@ io.helidon.bundles helidon-bundles-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-bundles-config diff --git a/bundles/pom.xml b/bundles/pom.xml index 048f8e7a97b..4e005cbda81 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT config diff --git a/bundles/security/pom.xml b/bundles/security/pom.xml index a24bec5a558..4eb18835863 100644 --- a/bundles/security/pom.xml +++ b/bundles/security/pom.xml @@ -22,8 +22,7 @@ io.helidon.bundles helidon-bundles-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT helidon-bundles-security diff --git a/codegen/apt/pom.xml b/codegen/apt/pom.xml index f8e916dbdb4..c66fa84db02 100644 --- a/codegen/apt/pom.xml +++ b/codegen/apt/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-apt diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java index 42f0e363f7b..5f080864e79 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java @@ -16,53 +16,29 @@ package io.helidon.codegen.apt; +import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; +import java.util.Optional; 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.ExecutableElement; import javax.lang.model.util.Elements; import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; /** * Factory for annotations. */ +@SuppressWarnings("removal") final class AptAnnotationFactory { private AptAnnotationFactory() { } - /** - * Creates a set of annotations using annotation processor. - * - * @param annoMirrors the annotation type mirrors - * @param elements annotation processing element utils - * @return the annotation value set - */ - public static Set createAnnotations(List annoMirrors, Elements elements) { - return annoMirrors.stream() - .map(it -> createAnnotation(it, elements)) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - /** - * Creates a set of annotations based using annotation processor. - * - * @param type the enclosing/owing type element - * @param elements annotation processing element utils - * @return the annotation value set - */ - public static Set createAnnotations(Element type, Elements elements) { - return createAnnotations(type.getAnnotationMirrors(), elements); - } - /** * Creates an instance from an annotation mirror during annotation processing. * @@ -76,7 +52,43 @@ public static Annotation createAnnotation(AnnotationMirror am, .orElseThrow(() -> new IllegalArgumentException("Cannot create annotation for non-existent type: " + am.getAnnotationType())); - return Annotation.create(val, extractAnnotationValues(am, elements)); + // ignore these annotations, unless one of them was explicitly requested + var set = new HashSet(); + set.add(TypeNames.INHERITED); + set.add(TypeNames.TARGET); + set.add(TypeNames.RETENTION); + set.add(TypeNames.DOCUMENTED); + set.remove(val); + + return createAnnotation(elements, am, set) + .orElseThrow(); + } + + private static Optional createAnnotation(Elements elements, AnnotationMirror am, Set processedTypes) { + TypeName val = AptTypeFactory.createTypeName(am.getAnnotationType()) + .orElseThrow(() -> new IllegalArgumentException("Cannot create annotation for non-existent type: " + + am.getAnnotationType())); + + if (processedTypes.contains(val)) { + return Optional.empty(); + } + + Annotation.Builder builder = Annotation.builder(); + + elements.getAllAnnotationMirrors(am.getAnnotationType().asElement()) + .stream() + .map(it -> { + var newProcessed = new HashSet<>(processedTypes); + newProcessed.add(val); + return createAnnotation(elements, it, newProcessed); + }) + .flatMap(Optional::stream) + .forEach(builder::addMetaAnnotation); + + return Optional.of(builder + .typeName(val) + .values(extractAnnotationValues(am, elements)) + .build()); } /** diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java index 4cd2555d5a7..b8c769a45fd 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java @@ -16,16 +16,22 @@ package io.helidon.codegen.apt; +import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; import javax.annotation.processing.ProcessingEnvironment; import io.helidon.codegen.CodegenContext; import io.helidon.codegen.Option; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; /** * Annotation processing code generation context. + * @deprecated this API will be package local in the future, use through Helidon codegen only */ +@Deprecated(forRemoval = true, since = "4.1.0") public interface AptContext extends CodegenContext { /** * Create context from the processing environment, and a set of additional supported options. @@ -44,4 +50,14 @@ static AptContext create(ProcessingEnvironment env, Set> options) { * @return environment */ ProcessingEnvironment aptEnv(); + + /** + * Get a cached instance of the type info, and if not cached, cache the provided one. + * Only type infos known not to be modified during this build are cached. + * + * @param typeName type name + * @param typeInfoSupplier supplier of value if it is not yet cached + * @return type info for that name, in case the type info cannot be created, an empty optional + */ + Optional cache(TypeName typeName, Supplier> typeInfoSupplier); } diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java index 8ce02a3d4d8..db808af3cbf 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java @@ -19,9 +19,12 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -40,11 +43,14 @@ import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; +@SuppressWarnings("removal") class AptContextImpl extends CodegenContextBase implements AptContext { private static final Pattern SCOPE_PATTERN = Pattern.compile("(\\w+).*classes"); private final ProcessingEnvironment env; private final ModuleInfo moduleInfo; + private final Map> safeTypeCache = new HashMap<>(); + private final Map> typeCache = new HashMap<>(); AptContextImpl(ProcessingEnvironment env, CodegenOptions options, @@ -59,7 +65,7 @@ class AptContextImpl extends CodegenContextBase implements AptContext { this.moduleInfo = moduleInfo; } - static AptContext create(ProcessingEnvironment env, Set> supportedOptions) { + static AptContextImpl create(ProcessingEnvironment env, Set> supportedOptions) { CodegenOptions options = AptOptions.create(env); CodegenScope scope = guessScope(env, options); @@ -81,11 +87,16 @@ public ProcessingEnvironment aptEnv() { @Override public Optional typeInfo(TypeName typeName) { + if (typeCache.containsKey(typeName)) { + return typeCache.get(typeName); + } + // cached by the factory return AptTypeInfoFactory.create(this, typeName); } @Override public Optional typeInfo(TypeName typeName, Predicate elementPredicate) { + // cannot be cached return AptTypeInfoFactory.create(this, typeName, elementPredicate); } @@ -94,6 +105,39 @@ public Optional module() { return Optional.ofNullable(moduleInfo); } + @Override + public Optional cache(TypeName typeName, Supplier> typeInfoSupplier) { + if (typeName.generic() || !typeName.typeArguments().isEmpty() || !typeName.typeParameters().isEmpty()) { + // generic types cannot be cached + return typeInfoSupplier.get(); + } + + if (typeName.packageName().startsWith("java.") + || typeName.packageName().startsWith("javax.") + || typeName.packageName().startsWith("sun.") + || typeName.packageName().startsWith("com.sun")) { + Optional typeInfo = safeTypeCache.get(typeName); + if (typeInfo != null) { + return typeInfo; + } + typeInfo = typeInfoSupplier.get(); + safeTypeCache.put(typeName, typeInfo); + return typeInfo; + } + + Optional typeInfo = typeCache.get(typeName); + if (typeInfo != null) { + return typeInfo; + } + typeInfo = typeInfoSupplier.get(); + typeCache.put(typeName, typeInfo); + return typeInfo; + } + + void resetCache() { + typeCache.clear(); + } + private static Optional findModule(Filer filer) { // expected is source location try { @@ -101,7 +145,7 @@ private static Optional findModule(Filer filer) { try (InputStream in = resource.openInputStream()) { return Optional.of(ModuleInfoSourceParser.parse(in)); } - } catch (IOException ignored) { + } catch (Exception ignored) { // it is not in sources, let's see if it got generated } // generated diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java index 84a7bbf2fab..9efa87e8e72 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java @@ -17,6 +17,7 @@ package io.helidon.codegen.apt; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; @@ -35,9 +36,11 @@ import io.helidon.codegen.CodegenException; import io.helidon.codegen.CodegenFiler; import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.FilerResource; import io.helidon.codegen.FilerTextResource; import io.helidon.codegen.IndentType; import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeName; import static java.nio.charset.StandardCharsets.UTF_8; @@ -71,6 +74,23 @@ public Path writeSourceFile(ClassModel classModel, Object... originatingElements } } + @Override + public Path writeSourceFile(TypeName type, String content, Object... originatingElements) { + Element[] elements = toElements(originatingElements); + + try { + JavaFileObject sourceFile = filer.createSourceFile(type.fqName(), elements); + try (Writer os = sourceFile.openWriter()) { + os.write(content); + } + return Path.of(sourceFile.toUri()); + } catch (IOException e) { + throw new CodegenException("Failed to write source file for type: " + type, + e, + originatingElement(elements, type)); + } + } + @Override public Path writeResource(byte[] resource, String location, Object... originatingElements) { Element[] elements = toElements(originatingElements); @@ -107,6 +127,20 @@ public FilerTextResource textResource(String location, Object... originatingElem } } + @Override + public FilerResource resource(String location, Object... originatingElements) { + try { + var resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", location); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (var is = resource.openInputStream()) { + is.transferTo(baos); + } + return new FilerResourceImpl(filer, location, toElements(originatingElements), resource, baos.toByteArray()); + } catch (IOException e) { + return new FilerResourceImpl(filer, location, toElements(originatingElements)); + } + } + private Object originatingElement(Element[] elements, Object alternative) { if (elements.length == 0) { return alternative; diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java index 0d95a814394..7716e72b89e 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java @@ -17,12 +17,13 @@ package io.helidon.codegen.apt; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; @@ -35,7 +36,9 @@ import io.helidon.codegen.Codegen; import io.helidon.codegen.CodegenEvent; +import io.helidon.codegen.CodegenException; import io.helidon.codegen.Option; +import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; @@ -45,10 +48,11 @@ /** * Annotation processor that maps APT types to Helidon types, and invokes {@link io.helidon.codegen.Codegen}. */ +@SuppressWarnings("removal") public final class AptProcessor extends AbstractProcessor { private static final TypeName GENERATOR = TypeName.create(AptProcessor.class); - private AptContext ctx; + private AptContextImpl ctx; private Codegen codegen; /** @@ -66,13 +70,8 @@ public SourceVersion getSupportedSourceVersion() { @Override public Set getSupportedAnnotationTypes() { - return Stream.concat(codegen.supportedAnnotations() - .stream() - .map(TypeName::fqName), - codegen.supportedAnnotationPackagePrefixes() - .stream() - .map(it -> it + "*")) - .collect(Collectors.toSet()); + // we need to support all annotations, to be able to use meta-annotations + return Set.of("*"); } @Override @@ -87,12 +86,14 @@ public Set getSupportedOptions() { public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.ctx = AptContext.create(processingEnv, Codegen.supportedOptions()); + this.ctx = AptContextImpl.create(processingEnv, Codegen.supportedOptions()); this.codegen = Codegen.create(ctx, GENERATOR); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { + this.ctx.resetCache(); + Thread thread = Thread.currentThread(); ClassLoader previousClassloader = thread.getContextClassLoader(); thread.setContextClassLoader(AptProcessor.class.getClassLoader()); @@ -100,47 +101,130 @@ public boolean process(Set annotations, RoundEnvironment // we want everything to execute in the classloader of this type, so service loaders // use the classpath of the annotation processor, and not some "random" classloader, such as a maven one try { - doProcess(annotations, roundEnv); - return true; + return doProcess(annotations, roundEnv); + } catch (CodegenException e) { + Object originatingElement = e.originatingElement() + .orElse(null); + if (originatingElement instanceof Element element) { + processingEnv.getMessager().printError(e.getMessage(), element); + } else if (originatingElement instanceof TypeName typeName) { + processingEnv.getMessager().printError(e.getMessage() + ", source: " + typeName.fqName()); + } else { + if (originatingElement != null) { + processingEnv.getMessager().printError(e.getMessage() + ", source: " + originatingElement); + } + } + throw e; } finally { thread.setContextClassLoader(previousClassloader); } } - private void doProcess(Set annotations, RoundEnvironment roundEnv) { + private boolean doProcess(Set annotations, RoundEnvironment roundEnv) { ctx.logger().log(TRACE, "Process annotations: " + annotations + ", processing over: " + roundEnv.processingOver()); if (roundEnv.processingOver()) { codegen.processingOver(); - return; + return annotations.isEmpty(); } - if (annotations.isEmpty()) { + Set usedAnnotations = usedAnnotations(annotations); + + if (usedAnnotations.isEmpty()) { // no annotations, no types, still call the codegen, maybe it has something to do codegen.process(List.of()); - return; + return annotations.isEmpty(); } - List allTypes = discoverTypes(annotations, roundEnv); + List allTypes = discoverTypes(usedAnnotations, roundEnv); codegen.process(allTypes); + + return usedAnnotations.stream() + .map(UsedAnnotation::annotationElement) + .collect(Collectors.toSet()) + .equals(annotations); } - private List discoverTypes(Set annotations, RoundEnvironment roundEnv) { + private Set usedAnnotations(Set annotations) { + var typePredicate = typePredicate(codegen.supportedAnnotations(), codegen.supportedAnnotationPackagePrefixes()); + var metaPredicate = typePredicate(codegen.supportedMetaAnnotations(), Set.of()); + + Set result = new HashSet<>(); + + for (TypeElement annotation : annotations) { + TypeName typeName = TypeName.create(annotation.getQualifiedName().toString()); + + Set supportedAnnotations = new HashSet<>(); + // first check direct support (through exact type or prefix) + if (typePredicate.test(typeName)) { + supportedAnnotations.add(typeName); + } + /* + find meta annotations that are supported: + - annotation that annotates the current annotation + */ + addSupportedAnnotations(metaPredicate, supportedAnnotations, typeName); + + // and add all the annotations + if (!supportedAnnotations.isEmpty()) { + result.add(new UsedAnnotation(typeName, annotation, supportedAnnotations)); + } + } + + return result; + } + + private Predicate typePredicate(Set typeNames, Set prefixes) { + return typeName -> { + if (typeNames.contains(typeName)) { + return true; + } + + String packagePrefix = typeName.packageName() + "."; + for (String prefix : prefixes) { + if (packagePrefix.startsWith(prefix)) { + return true; + } + } + return false; + }; + } + + private void addSupportedAnnotations(Predicate typeNamePredicate, + Set supportedAnnotations, + TypeName annotationType) { + Optional foundInfo = AptTypeInfoFactory.create(ctx, annotationType); + if (foundInfo.isPresent()) { + TypeInfo annotationInfo = foundInfo.get(); + List annotations = annotationInfo.annotations(); + for (Annotation annotation : annotations) { + TypeName typeName = annotation.typeName(); + if (typeNamePredicate.test(typeName)) { + if (supportedAnnotations.add(typeName)) { + addSupportedAnnotations(typeNamePredicate, supportedAnnotations, typeName); + } + } + } + } + } + + private List discoverTypes(Set annotations, RoundEnvironment roundEnv) { // we must discover all types that should be handled, create TypeInfo and only then check if these should be processed // as we may replace annotations, elements, and whole types. // first collect all types (group by type name, so we do not have duplicity) Map types = new HashMap<>(); - for (TypeElement annotation : annotations) { - Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotation); + for (UsedAnnotation annotation : annotations) { + TypeElement annotationElement = annotation.annotationElement(); + Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotationElement); for (Element element : elementsAnnotatedWith) { ElementKind kind = element.getKind(); switch (kind) { - case ENUM, INTERFACE, CLASS, ANNOTATION_TYPE, RECORD -> addType(types, element, element, annotation); + case ENUM, INTERFACE, CLASS, ANNOTATION_TYPE, RECORD -> addType(types, element, element, annotationElement); case ENUM_CONSTANT, CONSTRUCTOR, METHOD, FIELD, STATIC_INIT, INSTANCE_INIT, RECORD_COMPONENT -> - addType(types, element.getEnclosingElement(), element, annotation); - case PARAMETER -> addType(types, element.getEnclosingElement().getEnclosingElement(), element, annotation); + addType(types, element.getEnclosingElement(), element, annotationElement); + case PARAMETER -> addType(types, element.getEnclosingElement().getEnclosingElement(), element, annotationElement); default -> ctx.logger().log(TRACE, "Ignoring annotated element, not supported: " + element + ", kind: " + kind); } } @@ -177,4 +261,16 @@ private void addType(Map types, processedElement); } } + + /** + * Annotation that annotates a processed type and that must be processed. + * + * @param annotationType annotation on processed type + * @param annotationElement element of the annotation + * @param supportedAnnotations annotations that are supported (either the actual annotation, or meta-annotations) + */ + private record UsedAnnotation(TypeName annotationType, + TypeElement annotationElement, + Set supportedAnnotations) { + } } diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java index f752bc8b0c8..83ddf357e66 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java @@ -17,9 +17,15 @@ package io.helidon.codegen.apt; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.lang.model.element.Element; @@ -31,17 +37,25 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; import static io.helidon.common.types.TypeName.createFromGenericDeclaration; /** * Factory for types. + * + * @deprecated this is intended for internal use. This will be a package private type in the future. */ +@Deprecated(forRemoval = true, since = "4.2.0") public final class AptTypeFactory { + private static final Pattern NESTED_TYPES = Pattern.compile("(? createTypeName(DeclaredType type) { * none or error) */ public static Optional createTypeName(TypeMirror typeMirror) { + return createTypeName(new HashSet<>(), typeMirror); + } + + private static Optional createTypeName(Set inProgress, TypeMirror typeMirror) { TypeKind kind = typeMirror.getKind(); if (kind.isPrimitive()) { Class type = switch (kind) { @@ -86,9 +104,35 @@ public static Optional createTypeName(TypeMirror typeMirror) { return Optional.of(TypeName.create(void.class)); } case TYPEVAR -> { - return Optional.of(createFromGenericDeclaration(typeMirror.toString())); + if (!inProgress.add(typeMirror)) { + return Optional.empty(); // prevent infinite loop + } + + try { + var builder = TypeName.builder(createFromGenericDeclaration(typeMirror.toString())); + + var typeVar = ((TypeVariable) typeMirror); + handleBounds(inProgress, typeVar.getUpperBound(), builder::addUpperBound); + handleBounds(inProgress, typeVar.getLowerBound(), builder::addLowerBound); + + return Optional.of(builder.build()); + } finally { + inProgress.remove(typeMirror); + } + } + case WILDCARD -> { + WildcardType vt = ((WildcardType) typeMirror); + var builder = TypeName.builder() + .generic(true) + .wildcard(true) + .className("?"); + + handleBounds(inProgress, vt.getExtendsBound(), builder::addUpperBound); + handleBounds(inProgress, vt.getSuperBound(), builder::addLowerBound); + + return Optional.of(builder.build()); } - case WILDCARD, ERROR -> { + case ERROR -> { return Optional.of(TypeName.create(typeMirror.toString())); } // this is most likely a type that is code generated as part of this round, best effort @@ -101,7 +145,7 @@ public static Optional createTypeName(TypeMirror typeMirror) { } if (typeMirror instanceof ArrayType arrayType) { - return Optional.of(TypeName.builder(createTypeName(arrayType.getComponentType()).orElseThrow()) + return Optional.of(TypeName.builder(createTypeName(inProgress, arrayType.getComponentType()).orElseThrow()) .array(true) .build()); } @@ -109,21 +153,47 @@ public static Optional createTypeName(TypeMirror typeMirror) { if (typeMirror instanceof DeclaredType declaredType) { List typeParams = declaredType.getTypeArguments() .stream() - .map(AptTypeFactory::createTypeName) + .map(it -> createTypeName(inProgress, it)) .flatMap(Optional::stream) .collect(Collectors.toList()); - TypeName result = createTypeName(declaredType.asElement()).orElse(null); + TypeName result = createTypeName(inProgress, declaredType.asElement()).orElse(null); if (typeParams.isEmpty() || result == null) { return Optional.ofNullable(result); } + if (!inProgress.add(typeMirror)) { + return Optional.empty(); // prevent infinite loop + } return Optional.of(TypeName.builder(result).typeArguments(typeParams).build()); } throw new IllegalStateException("Unknown type mirror: " + typeMirror); } + private static void handleBounds(Set processed, TypeMirror boundMirror, Consumer boundHandler) { + if (boundMirror == null) { + return; + } + if (boundMirror.getKind() != TypeKind.NULL) { + if (boundMirror.getKind() == TypeKind.INTERSECTION) { + IntersectionType it = (IntersectionType) boundMirror; + it.getBounds() + .stream() + .filter(Predicate.not(processed::equals)) + .map(typeMirror -> createTypeName(processed, typeMirror)) + .flatMap(Optional::stream) + .filter(Predicate.not(TypeNames.OBJECT::equals)) + .forEach(boundHandler); + + } else { + createTypeName(processed, boundMirror) + .filter(Predicate.not(TypeNames.OBJECT::equals)) + .ifPresent(boundHandler); + } + } + } + /** * Create type from type mirror. The element is needed to correctly map * type arguments to type parameters. @@ -133,7 +203,7 @@ public static Optional createTypeName(TypeMirror typeMirror) { * @return type for the provided values */ public static Optional createTypeName(TypeElement element, TypeMirror mirror) { - Optional result = AptTypeFactory.createTypeName(mirror); + Optional result = createTypeName(new HashSet<>(), mirror); if (result.isEmpty()) { return result; } @@ -161,17 +231,29 @@ public static Optional createTypeName(TypeElement element, TypeMirror * @return the associated type name instance */ public static Optional createTypeName(Element type) { + return createTypeName(new HashSet<>(), type); + } + + private static Optional createTypeName(Set processed, Element type) { if (type instanceof VariableElement) { - return createTypeName(type.asType()); + return createTypeName(processed, type.asType()); } - if (type instanceof ExecutableElement) { - return createTypeName(((ExecutableElement) type).getReturnType()); + if (type instanceof ExecutableElement ee) { + return createTypeName(processed, ee.getReturnType()); } List classNames = new ArrayList<>(); String simpleName = type.getSimpleName().toString(); + // if there is a single $, we consider that to be an inner class + String[] split = NESTED_TYPES.split(simpleName); + if (split.length > 1) { + classNames.addAll(Arrays.asList(split) + .subList(0, split.length - 1)); + simpleName = split[split.length - 1]; + } + Element enclosing = type.getEnclosingElement(); while (enclosing != null && ElementKind.PACKAGE != enclosing.getKind()) { if (enclosing.getKind() == ElementKind.CLASS @@ -182,6 +264,7 @@ public static Optional createTypeName(Element type) { } enclosing = enclosing.getEnclosingElement(); } + Collections.reverse(classNames); // try to find the package diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java index d044bc99704..b5a42510ed8 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java @@ -64,7 +64,13 @@ /** * Factory to analyze processed types and to provide {@link io.helidon.common.types.TypeInfo} for them. + * + * @deprecated this is an internal API, all usage should be done through {@code helidon-codegen} APIs, + * such as {@link io.helidon.codegen.CodegenContext#typeInfo(io.helidon.common.types.TypeName)}; + * this type will be package local in the future */ +@SuppressWarnings("removal") +@Deprecated(forRemoval = true) public final class AptTypeInfoFactory extends TypeInfoFactoryBase { // we expect that annotations themselves are not code generated, and can be cached @@ -116,7 +122,10 @@ public static Optional create(AptContext ctx, * @return type info for the type element * @throws IllegalArgumentException when the element cannot be resolved into type info (such as if you ask for * a primitive type) + * @deprecated this is an internal API, all usage should be done through {@code helidon-codegen} APIs, + * such as {@link io.helidon.codegen.CodegenContext#typeInfo(io.helidon.common.types.TypeName)} */ + @Deprecated(forRemoval = true) public static Optional create(AptContext ctx, TypeElement typeElement) { @@ -177,7 +186,8 @@ public static Optional createTypedElementInfoFromElement(AptCo if (v instanceof ExecutableElement ee) { typeMirror = Objects.requireNonNull(ee.getReturnType()); - params = ee.getParameters().stream() + params = ee.getParameters() + .stream() .map(it -> createTypedElementInfoFromElement(ctx, processedType, it, elements).orElseThrow(() -> { return new CodegenException("Failed to create element info for parameter: " + it + ", either it uses " + "invalid type, or it was removed by an element mapper. This would" @@ -245,7 +255,13 @@ public static Optional createTypedElementInfoFromElement(AptCo .throwsChecked(thrownChecked) .parameterArguments(params) .originatingElement(v); - AptTypeFactory.createTypeName(v.getEnclosingElement()).ifPresent(builder::enclosingType); + + // To be failure-tolerant, as the ECJ may not provide an enclosing element for a VariableElement. + Element enclosingElement = v.getEnclosingElement(); + if (enclosingElement != null) { + AptTypeFactory.createTypeName(enclosingElement).ifPresent(builder::enclosingType); + } + Optional.ofNullable(defaultValue).ifPresent(builder::defaultValue); return mapElement(ctx, builder.build()); @@ -390,6 +406,19 @@ private static Optional create(AptContext ctx, // Object is not to be analyzed return Optional.empty(); } + + if (elementPredicate == ElementInfoPredicates.ALL_PREDICATE) { + // we can safely cache + return ctx.cache(typeName, () -> createUncached(ctx, typeElement, elementPredicate, typeName)); + } + + return createUncached(ctx, typeElement, elementPredicate, typeName); + } + + private static Optional createUncached(AptContext ctx, + TypeElement typeElement, + Predicate elementPredicate, + TypeName typeName) { TypeName genericTypeName = typeName.genericTypeName(); Set allInterestingTypeNames = new LinkedHashSet<>(); allInterestingTypeNames.add(genericTypeName); @@ -402,8 +431,14 @@ private static Optional create(AptContext ctx, Elements elementUtils = ctx.aptEnv().getElementUtils(); try { + TypeElement foundType = elementUtils.getTypeElement(genericTypeName.resolvedName()); + if (foundType == null) { + // this is probably forward referencing a generated type, ignore + return Optional.empty(); + } + TypeName declaredTypeName = declaredTypeName(ctx, genericTypeName); List annotations = createAnnotations(ctx, - elementUtils.getTypeElement(genericTypeName.resolvedName()), + foundType, elementUtils); List inheritedAnnotations = createInheritedAnnotations(ctx, genericTypeName, annotations); @@ -417,27 +452,19 @@ private static Optional create(AptContext ctx, typeElement.getEnclosedElements() .stream() .flatMap(it -> createTypedElementInfoFromElement(ctx, genericTypeName, it, elementUtils).stream()) - .forEach(it -> { - if (elementPredicate.test(it)) { - elementsWeCareAbout.add(it); - } else { - otherElements.add(it); - } - annotationsOnTypeOrElements.addAll(it.annotations() - .stream() - .map(Annotation::typeName) - .collect(Collectors.toSet())); - it.parameterArguments() - .forEach(arg -> annotationsOnTypeOrElements.addAll(arg.annotations() - .stream() - .map(Annotation::typeName) - .collect(Collectors.toSet()))); - }); + .forEach(it -> collectEnclosedElements(elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements, + it)); + Set modifiers = toModifierNames(typeElement.getModifiers()); TypeInfo.Builder builder = TypeInfo.builder() .originatingElement(typeElement) .typeName(typeName) + .rawType(genericTypeName) + .declaredType(declaredTypeName) .kind(kind(typeElement.getKind())) .annotations(annotations) .inheritedAnnotations(inheritedAnnotations) @@ -530,6 +557,33 @@ private static Optional create(AptContext ctx, } } + private static TypeName declaredTypeName(AptContext ctx, TypeName typeName) { + TypeElement typeElement = ctx.aptEnv().getElementUtils().getTypeElement(typeName.fqName()); + // we know this type exists, we do not have to check for null + return AptTypeFactory.createTypeName(typeElement.asType()).orElseThrow(); + } + + private static void collectEnclosedElements(Predicate elementPredicate, + List elementsWeCareAbout, + List otherElements, + Set annotationsOnTypeOrElements, + TypedElementInfo enclosedElement) { + if (elementPredicate.test(enclosedElement)) { + elementsWeCareAbout.add(enclosedElement); + } else { + otherElements.add(enclosedElement); + } + annotationsOnTypeOrElements.addAll(enclosedElement.annotations() + .stream() + .map(Annotation::typeName) + .collect(Collectors.toSet())); + enclosedElement.parameterArguments() + .forEach(arg -> annotationsOnTypeOrElements.addAll(arg.annotations() + .stream() + .map(Annotation::typeName) + .collect(Collectors.toSet()))); + } + private static AccessModifier accessModifier(Set stringModifiers) { for (String stringModifier : stringModifiers) { try { diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/FilerResourceImpl.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/FilerResourceImpl.java new file mode 100644 index 00000000000..4f1915bf248 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/FilerResourceImpl.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.apt; + +import java.util.Arrays; + +import javax.annotation.processing.Filer; +import javax.lang.model.element.Element; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.FilerResource; + +class FilerResourceImpl implements FilerResource { + private final Filer filer; + private final String location; + private final Element[] originatingElements; + private final FileObject originalResource; // may be null + + private byte[] currentBytes; + + private boolean modified; + + FilerResourceImpl(Filer filer, String location, Element[] originatingElements) { + this.filer = filer; + this.location = location; + this.originatingElements = originatingElements; + this.originalResource = null; + this.currentBytes = new byte[0]; + } + + FilerResourceImpl(Filer filer, + String location, + Element[] originatingElements, + FileObject originalResource, + byte[] existingBytes) { + this.filer = filer; + this.location = location; + this.originatingElements = originatingElements; + this.originalResource = originalResource; + this.currentBytes = existingBytes; + } + + @Override + public byte[] bytes() { + return Arrays.copyOf(currentBytes, currentBytes.length); + } + + @Override + public void bytes(byte[] newBytes) { + currentBytes = Arrays.copyOf(newBytes, newBytes.length); + modified = true; + } + + @Override + public void write() { + if (modified) { + if (originalResource != null) { + originalResource.delete(); + } + try { + FileObject newResource = filer.createResource(StandardLocation.CLASS_OUTPUT, + "", + location, + originatingElements); + try (var os = newResource.openOutputStream()) { + os.write(currentBytes); + } + } catch (Exception e) { + throw new CodegenException("Failed to create resource: " + location, e); + } + } + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/FilerTextResourceImpl.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/FilerTextResourceImpl.java index f0cfb8ed4f6..c694c4674f1 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/FilerTextResourceImpl.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/FilerTextResourceImpl.java @@ -76,7 +76,11 @@ public void lines(List newLines) { public void write() { if (modified) { if (originalResource != null) { - originalResource.delete(); + try { + originalResource.delete(); + } catch (Exception ignored) { + // The resource cannot be deleted, e.g. because ECJ has not implemented this method. + } } try { FileObject newResource = filer.createResource(StandardLocation.CLASS_OUTPUT, diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java index 58cb031ceec..15f8e5f0bc6 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java @@ -28,6 +28,9 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; +import io.helidon.common.types.EnumValue; +import io.helidon.common.types.TypeNames; + class ToAnnotationValueVisitor implements AnnotationValueVisitor { private final Elements elements; private boolean mapVoidToNull; @@ -126,18 +129,30 @@ public Object visitString(String s, Object o) { return s; } + @SuppressWarnings("removal") @Override public Object visitType(TypeMirror t, Object o) { - String val = t.toString(); - if (mapVoidToNull && ("void".equals(val) || Void.class.getName().equals(val))) { - val = null; + var maybeType = AptTypeFactory.createTypeName(t); + if (maybeType.isEmpty()) { + return null; + } + var type = maybeType.get(); + if (mapVoidToNull && (type.equals(TypeNames.BOXED_VOID) || type.equals(TypeNames.PRIMITIVE_VOID))) { + return null; } - return val; + return type; } + @SuppressWarnings("removal") @Override public Object visitEnumConstant(VariableElement c, Object o) { - return String.valueOf(c.getSimpleName()); + var maybeType = AptTypeFactory.createTypeName(c.getEnclosingElement()); + + if (maybeType.isEmpty()) { + // this will be one-way only + return String.valueOf(c.getSimpleName()); + } + return EnumValue.create(maybeType.get(), String.valueOf(c.getSimpleName())); } @Override diff --git a/codegen/class-model/pom.xml b/codegen/class-model/pom.xml index 9c4eb7a3bdd..60bb27f93fc 100644 --- a/codegen/class-model/pom.xml +++ b/codegen/class-model/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-class-model diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java index e13b6783f07..86cde78ce0e 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java @@ -34,8 +34,13 @@ void addImports(ImportOrganizer.Builder imports) { annotations.forEach(annotation -> annotation.addImports(imports)); } - List annotations() { - return annotations; + /** + * List of annotations on this component. + * + * @return annotations + */ + public List annotations() { + return List.copyOf(annotations); } abstract static class Builder, T extends AnnotatedComponent> extends CommonComponent.Builder { @@ -67,11 +72,7 @@ public B addDescriptionLine(String line) { * @return updated builder instance */ public B addAnnotation(io.helidon.common.types.Annotation annotation) { - return addAnnotation(newAnnot -> { - newAnnot.type(annotation.typeName()); - annotation.values() - .forEach(newAnnot::addParameter); - }); + return addAnnotation(Annotation.create(annotation)); } /** diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java index c52c7a56f1c..b84bbf95365 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java @@ -31,10 +31,12 @@ public final class Annotation extends CommonComponent { private final List parameters; + private final io.helidon.common.types.Annotation commonAnnotation; private Annotation(Builder builder) { super(builder); this.parameters = List.copyOf(builder.parameters.values()); + this.commonAnnotation = builder.commonAnntation; } /** @@ -88,6 +90,35 @@ public static Annotation parse(String annotationDefinition) { return builder.build(); } + /** + * Create a class model annotation from common types annotation. + * + * @param annotation annotation to process + * @return a new class model annotation + */ + public static Annotation create(io.helidon.common.types.Annotation annotation) { + return builder().from(annotation).build(); + } + + /** + * Convert class model annotation to Helidon Common Types annotation. + * + * @return common types annotation + */ + public io.helidon.common.types.Annotation toTypesAnnotation() { + if (this.commonAnnotation != null) { + return commonAnnotation; + } + var builder = io.helidon.common.types.Annotation.builder() + .typeName(type().genericTypeName()); + + for (AnnotationParameter parameter : parameters) { + builder.putValue(parameter.name(), parameter.value()); + } + + return builder.build(); + } + @Override void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { @@ -97,7 +128,7 @@ void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrgani if (parameters.size() == 1) { AnnotationParameter parameter = parameters.get(0); if (parameter.name().equals("value")) { - writer.write(parameter.value()); + parameter.writeValue(writer, imports); } else { parameter.writeComponent(writer, declaredTokens, imports, classType); } @@ -128,6 +159,7 @@ void addImports(ImportOrganizer.Builder imports) { public static final class Builder extends CommonComponent.Builder { private final Map parameters = new LinkedHashMap<>(); + private io.helidon.common.types.Annotation commonAnntation; private Builder() { } @@ -210,6 +242,13 @@ public Builder addParameter(AnnotationParameter parameter) { return this; } + Builder from(io.helidon.common.types.Annotation annotation) { + this.commonAnntation = annotation; + type(annotation.typeName()); + annotation.values() + .forEach(this::addParameter); + return this; + } } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java index b4a28580262..1b07dd3633d 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java @@ -16,9 +16,15 @@ package io.helidon.codegen.classmodel; import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.EnumValue; import io.helidon.common.types.TypeName; /** @@ -26,11 +32,14 @@ */ public final class AnnotationParameter extends CommonComponent { - private final String value; + private final Set importedTypes; + private final Object objectValue; private AnnotationParameter(Builder builder) { super(builder); - this.value = resolveValueToString(builder.type(), builder.value); + + this.objectValue = builder.value; + this.importedTypes = resolveImports(builder.value); } /** @@ -42,29 +51,139 @@ public static Builder builder() { return new Builder(); } + @Override + public String toString() { + return objectValue + " (" + type().simpleTypeName() + ")"; + } + @Override void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { - writer.write(name() + " = " + value); + writer.write(name() + " = "); + writeValue(writer, imports); + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + importedTypes.forEach(imports::addImport); } - private static String resolveValueToString(Type type, Object value) { + void writeValue(ModelWriter writer, ImportOrganizer imports) throws IOException { + writer.write(resolveValueToString(imports, type(), objectValue)); + } + + Object value() { + return objectValue; + } + + private static Set resolveImports(Object value) { + Set imports = new HashSet<>(); + + resolveImports(imports, value); + + return imports; + } + + private static void resolveImports(Set imports, Object value) { + if (value.getClass().isEnum()) { + imports.add(TypeName.create(value.getClass())); + return; + } + switch (value) { + case TypeName tn -> imports.add(tn); + case EnumValue ev -> imports.add(ev.type()); + case Annotation an -> { + imports.add(an.typeName()); + an.values() + .values() + .forEach(nestedValue -> resolveImports(imports, nestedValue)); + } + default -> { + } + } + } + + // takes the annotation value objects and converts it to its string representation (as seen in class source) + private static String resolveValueToString(ImportOrganizer imports, Type type, Object value) { Class valueClass = value.getClass(); if (valueClass.isEnum()) { - return valueClass.getSimpleName() + "." + ((Enum) value).name(); - } else if (type.fqTypeName().equals(String.class.getName())) { + return imports.typeName(Type.fromTypeName(TypeName.create(valueClass)), true) + + "." + ((Enum) value).name(); + } + if (type != null && type.fqTypeName().equals(String.class.getName())) { String stringValue = value.toString(); if (!stringValue.startsWith("\"") && !stringValue.endsWith("\"")) { return "\"" + stringValue + "\""; } - } else if (value instanceof TypeName typeName) { - return typeName.fqName() + ".class"; + return stringValue; + } + + if (type != null && type.fqTypeName().equals(Object.class.getName())) { + // we expect this to be "as is" - such as when parsing annotations + return value.toString(); + } + + return switch (value) { + case TypeName typeName -> imports.typeName(Type.fromTypeName(typeName), true) + ".class"; + case EnumValue enumValue -> imports.typeName(Type.fromTypeName(enumValue.type()), true) + + "." + enumValue.name(); + case Character character -> "'" + character + "'"; + case Long longValue -> longValue + "L"; + case Float floatValue -> floatValue + "F"; + case Double doubleValue -> doubleValue + "D"; + case Byte byteValue -> "(byte) " + byteValue; + case Short shortValue -> "(short) " + shortValue; + case Class clazz -> imports.typeName(Type.fromTypeName(TypeName.create(clazz)), true) + ".class"; + case Annotation annotation -> nestedAnnotationValue(imports, annotation); + case List list -> nestedListValue(imports, list); + case String str -> str.startsWith("\"") && str.endsWith("\"") ? str : "\"" + str + "\""; + default -> value.toString(); + }; + + } + + private static String nestedListValue(ImportOrganizer imports, List list) { + if (list.isEmpty()) { + return "{}"; + } + StringBuilder result = new StringBuilder(); + if (list.size() > 1) { + result.append("{"); + } + + result.append(list.stream() + .map(it -> resolveValueToString(imports, null, it)) + .collect(Collectors.joining(", "))); + + if (list.size() > 1) { + result.append("}"); } - return value.toString(); + return result.toString(); } - String value() { - return value; + private static String nestedAnnotationValue(ImportOrganizer imports, Annotation annotation) { + StringBuilder sb = new StringBuilder("@"); + sb.append(imports.typeName(Type.fromTypeName(annotation.typeName()), true)); + + Map values = annotation.values(); + if (values.isEmpty()) { + return sb.toString(); + } + + sb.append("("); + if (values.size() == 1 && values.containsKey("value")) { + sb.append(resolveValueToString(imports, null, values.get("value"))); + } else { + values.forEach((key, value) -> { + sb.append(key) + .append(" = ") + .append(resolveValueToString(imports, null, value)) + .append(", "); + }); + sb.delete(sb.length() - 2, sb.length()); + } + sb.append(")"); + return sb.toString(); } /** diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java index 611f3826c0f..0784de2055f 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java @@ -19,10 +19,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; 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.Supplier; @@ -81,36 +83,100 @@ public abstract class ClassBase extends AnnotatedComponent { this.superType = builder.superType; } - private static int methodCompare(Method method1, Method method2) { - if (method1.accessModifier() == method2.accessModifier()) { - return 0; - } else { - return method1.accessModifier().compareTo(method2.accessModifier()); - } + /** + * All declared fields. + * + * @return fields + */ + public List fields() { + return List.copyOf(fields); } - private static int fieldComparator(Field field1, Field field2) { - //This is here for ordering purposes. - if (field1.accessModifier() == field2.accessModifier()) { - if (field1.isFinal() == field2.isFinal()) { - if (field1.type().simpleTypeName().equals(field2.type().simpleTypeName())) { - if (field1.type().resolvedTypeName().equals(field2.type().resolvedTypeName())) { - return field1.name().compareTo(field2.name()); - } - return field1.type().resolvedTypeName().compareTo(field2.type().resolvedTypeName()); - } else if (field1.type().simpleTypeName().equalsIgnoreCase(field2.type().simpleTypeName())) { - //To ensure that types with the types with the same name, - //but with the different capital letters, will not be mixed - return field1.type().simpleTypeName().compareTo(field2.type().simpleTypeName()); - } - //ignoring case sensitivity to ensure primitive types are properly sorted - return field1.type().simpleTypeName().compareToIgnoreCase(field2.type().simpleTypeName()); - } - //final fields should be before non-final - return Boolean.compare(field2.isFinal(), field1.isFinal()); - } else { - return field1.accessModifier().compareTo(field2.accessModifier()); - } + /** + * All declared methods. + * + * @return methods + */ + public List methods() { + return List.copyOf(methods); + } + + /** + * All declared inner classes. + * + * @return inner classes + */ + public List innerClasses() { + return List.copyOf(innerClasses); + } + + /** + * All declared constructors. + * + * @return constructors + */ + public List constructors() { + return List.copyOf(constructors); + } + + /** + * Kind of this type. + * + * @return kind + */ + public ElementKind kind() { + return switch (classType) { + case CLASS -> ElementKind.CLASS; + case INTERFACE -> ElementKind.INTERFACE; + }; + } + + /** + * Type name of the super class (if this is a class and it extends another class). + * + * @return super type + */ + public Optional superTypeName() { + return Optional.ofNullable(superType) + .map(Type::genericTypeName); + } + + /** + * Implemented interfaces. + * + * @return interfaces this type implements (or extends, if this is an interface) + */ + public List interfaceTypeNames() { + return interfaces.stream() + .map(Type::genericTypeName) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * Is this a final class. + * + * @return whether this class is final + */ + public boolean isFinal() { + return isFinal; + } + + /** + * Is this an abstract class. + * + * @return whether this class is abstract + */ + public boolean isAbstract() { + return isAbstract; + } + + /** + * Is this a static class. + * + * @return whether this class is static + */ + public boolean isStatic() { + return isStatic; } @Override @@ -179,6 +245,67 @@ void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrgani writer.write("}"); } + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + fields.forEach(field -> field.addImports(imports)); + staticFields.forEach(field -> field.addImports(imports)); + methods.forEach(method -> method.addImports(imports)); + staticMethods.forEach(method -> method.addImports(imports)); + interfaces.forEach(imp -> imp.addImports(imports)); + constructors.forEach(constructor -> constructor.addImports(imports)); + genericParameters.forEach(param -> param.addImports(imports)); + innerClasses.forEach(innerClass -> { + imports.from(innerClass.imports()); + innerClass.addImports(imports); + }); + if (superType != null) { + superType.addImports(imports); + } + } + + ClassType classType() { + return classType; + } + + Map innerClassesMap() { + Map result = new HashMap<>(); + innerClasses.forEach(innerClass -> result.put(innerClass.name(), innerClass)); + return result; + } + + private static int methodCompare(Method method1, Method method2) { + if (method1.accessModifier() == method2.accessModifier()) { + return 0; + } else { + return method1.accessModifier().compareTo(method2.accessModifier()); + } + } + + private static int fieldComparator(Field field1, Field field2) { + //This is here for ordering purposes. + if (field1.accessModifier() == field2.accessModifier()) { + if (field1.isFinal() == field2.isFinal()) { + if (field1.type().simpleTypeName().equals(field2.type().simpleTypeName())) { + if (field1.type().resolvedTypeName().equals(field2.type().resolvedTypeName())) { + return field1.name().compareTo(field2.name()); + } + return field1.type().resolvedTypeName().compareTo(field2.type().resolvedTypeName()); + } else if (field1.type().simpleTypeName().equalsIgnoreCase(field2.type().simpleTypeName())) { + //To ensure that types with the types with the same name, + //but with the different capital letters, will not be mixed + return field1.type().simpleTypeName().compareTo(field2.type().simpleTypeName()); + } + //ignoring case sensitivity to ensure primitive types are properly sorted + return field1.type().simpleTypeName().compareToIgnoreCase(field2.type().simpleTypeName()); + } + //final fields should be before non-final + return Boolean.compare(field2.isFinal(), field1.isFinal()); + } else { + return field1.accessModifier().compareTo(field2.accessModifier()); + } + } + private void writeGenericParameters(ModelWriter writer, Set declaredTokens, ImportOrganizer imports) throws IOException { writer.write("<"); @@ -261,29 +388,6 @@ private void writeInnerClasses(ModelWriter writer, Set declaredTokens, I writer.decreasePaddingLevel(); } - @Override - void addImports(ImportOrganizer.Builder imports) { - super.addImports(imports); - fields.forEach(field -> field.addImports(imports)); - staticFields.forEach(field -> field.addImports(imports)); - methods.forEach(method -> method.addImports(imports)); - staticMethods.forEach(method -> method.addImports(imports)); - interfaces.forEach(imp -> imp.addImports(imports)); - constructors.forEach(constructor -> constructor.addImports(imports)); - genericParameters.forEach(param -> param.addImports(imports)); - innerClasses.forEach(innerClass -> { - imports.from(innerClass.imports()); - innerClass.addImports(imports); - }); - if (superType != null) { - superType.addImports(imports); - } - } - - ClassType classType() { - return classType; - } - /** * Fluent API builder for {@link ClassBase}. * @@ -687,5 +791,9 @@ B isStatic(boolean isStatic) { ImportOrganizer.Builder importOrganizer() { return importOrganizer; } + + Map innerClasses() { + return innerClasses; + } } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java index 4b935235ab7..bb8b61a358b 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java @@ -17,6 +17,9 @@ import java.io.IOException; import java.io.Writer; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; import io.helidon.common.types.AccessModifier; @@ -209,6 +212,49 @@ public Builder type(TypeName type) { return this; } - } + /** + * Find if the provided type name is handled as part of this generated class. + * + * @param typeName type name to look for + * @return class base that matches the provided type name + */ + public Optional find(TypeName typeName) { + if (!typeName.packageName().equals(packageName)) { + return Optional.empty(); + } + if (typeName.classNameWithEnclosingNames().equals(name())) { + return Optional.of(build()); + } + + List enclosingNames = typeName.enclosingNames(); + if (enclosingNames.isEmpty()) { + // did not hit above, will not hit below + return Optional.empty(); + } + String topLevel = enclosingNames.getFirst(); + if (!topLevel.equals(name())) { + // not an inner class of this class + return Optional.empty(); + } + // look for inner classes, ignoring this class + Map innerClasses = super.innerClasses(); + + InnerClass inProgress = null; + for (int i = 1; i < enclosingNames.size(); i++) { + String enclosingName = enclosingNames.get(i); + InnerClass found = innerClasses.get(enclosingName); + if (found == null) { + return Optional.empty(); + } + inProgress = found; + innerClasses = inProgress.innerClassesMap(); + } + if (inProgress == null) { + return Optional.ofNullable(innerClasses.get(typeName.className())); + } + // we found an inner class that matches the full hierarchy + return Optional.ofNullable(inProgress.innerClassesMap().get(typeName.className())); + } + } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java index 1dc0c9f07c0..d0467c753d4 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java @@ -33,7 +33,12 @@ abstract class CommonComponent extends DescribableComponent { this.javadoc = builder.javadocBuilder.build(builder); } - String name() { + /** + * Name of this component. + * + * @return component name + */ + public String name() { return name; } @@ -41,7 +46,12 @@ Javadoc javadoc() { return javadoc; } - AccessModifier accessModifier() { + /** + * Access modifier of this component. + * + * @return access modifier + */ + public AccessModifier accessModifier() { return accessModifier; } @@ -216,6 +226,11 @@ B accessModifier(AccessModifier accessModifier) { return identity(); } + /** + * Name of this component. + * + * @return component name + */ String name() { return name; } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java index bded3b90bc0..64b6e4fcc4e 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java @@ -153,6 +153,11 @@ public int hashCode() { return Objects.hash(isArray(), typeName.resolvedName()); } + @Override + TypeName typeName() { + return typeName; + } + static final class Builder extends ModelComponent.Builder { private final List typeParams = new ArrayList<>(); private TypeName typeName; diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java index 4237f75f8ce..109c787e529 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java @@ -19,6 +19,7 @@ import java.util.List; import io.helidon.common.types.Annotation; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; @@ -110,6 +111,25 @@ default T addContentCreate(TypeName typeName) { return addContent(""); } + /** + * Add content that creates a new {@link io.helidon.common.types.ResolvedType} in the generated code that is the same as the + * type name provided. + *

    + * To create a type name without type arguments (such as when used with {@code .class}), use + * {@link io.helidon.common.types.TypeName#genericTypeName()}. + *

    + * The generated content will be similar to: {@code TypeName.create("some.type.Name")} + * + * @param type type name to code generate + * @return updated builder instance + */ + default T addContentCreate(ResolvedType type) { + return addContent(ResolvedType.class) + .addContent(".create(\"") + .addContent(type.resolvedName()) + .addContent("\")"); + } + /** * Add content that creates a new {@link io.helidon.common.types.Annotation} in the generated code that is the same as the * annotation provided. diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java index 58b513935c3..26dc36a78fa 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java @@ -23,6 +23,7 @@ import io.helidon.common.types.AccessModifier; import io.helidon.common.types.Annotation; import io.helidon.common.types.ElementKind; +import io.helidon.common.types.EnumValue; import io.helidon.common.types.Modifier; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; @@ -84,7 +85,7 @@ static void addCreateElement(ContentBuilder contentBuilder, TypedElementInfo Set modifiers = element.elementModifiers(); for (Modifier modifier : modifiers) { - contentBuilder.addContent(".addModifier(") + contentBuilder.addContent(".addElementModifier(") .addContent(MODIFIER) .addContent(".") .addContent(modifier.name()) @@ -105,7 +106,7 @@ static void addCreateElement(ContentBuilder contentBuilder, TypedElementInfo static void addCreateAnnotation(ContentBuilder contentBuilder, Annotation annotation) { Map values = annotation.values(); - if (values.isEmpty()) { + if (values.isEmpty() && annotation.metaAnnotations().isEmpty()) { // Annotation.create(TypeName.create("my.type.AnnotationType")) contentBuilder.addContent(ANNOTATION) .addContent(".create(") @@ -135,6 +136,16 @@ static void addCreateAnnotation(ContentBuilder contentBuilder, Annotation ann contentBuilder.addContentLine(")"); }); + // .addMetaAnnotation(...) + annotation.metaAnnotations() + .forEach(it -> contentBuilder.addContent(".addMetaAnnotation(") + .increaseContentPadding() + .increaseContentPadding() + .addContentCreate(it) + .addContentLine(")") + .decreaseContentPadding() + .decreaseContentPadding()); + // .build() contentBuilder.addContentLine(".build()") .decreaseContentPadding() @@ -164,7 +175,9 @@ private static void addAnnotationValue(ContentBuilder contentBuilder, Object case Class value -> contentBuilder.addContentCreate(TypeName.create(value)); case TypeName value -> contentBuilder.addContentCreate(value); case Annotation value -> contentBuilder.addContentCreate(value); - case Enum value -> toEnumValue(contentBuilder, value); + case Enum value -> toEnumValue(contentBuilder, + EnumValue.create(TypeName.create(value.getDeclaringClass()), value.name())); + case EnumValue value -> toEnumValue(contentBuilder, value); case List values -> toListValues(contentBuilder, values); default -> throw new IllegalStateException("Unexpected annotation value type " + objectValue.getClass() .getName() + ": " + objectValue); @@ -185,9 +198,17 @@ private static void toListValues(ContentBuilder contentBuilder, List value contentBuilder.addContent(")"); } - private static void toEnumValue(ContentBuilder contentBuilder, Enum enumValue) { - contentBuilder.addContent(enumValue.getDeclaringClass()) - .addContent(".") - .addContent(enumValue.name()); + private static void toEnumValue(ContentBuilder contentBuilder, EnumValue enumValue) { + // it would be easier to just use Enum.VALUE, but annotations and their dependencies + // may not be on runtime classpath, so we have to work around it + + // EnumValue.create(TypeName.create(...), "VALUE") + contentBuilder.addContent(EnumValue.class) + .addContent(".create(") + .addContentCreate(enumValue.type()) + .addContent(",") + .addContent("\"") + .addContent(enumValue.name()) + .addContent("\")"); } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java index 956a8f6efcb..d9976eb00e6 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java @@ -36,10 +36,25 @@ Type type() { return type; } - List description() { + /** + * Description (javadoc) of this component. + * + * @return description lines + */ + public List description() { return description; } + /** + * Type name of this component. + * + * @return type name + */ + public TypeName typeName() { + return type().typeName(); + } + + @Override void addImports(ImportOrganizer.Builder imports) { if (includeImport() && type != null) { diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java index 3e1866d86a8..5f3bda7881b 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; import io.helidon.common.types.AccessModifier; import io.helidon.common.types.TypeName; @@ -54,10 +55,10 @@ void addImports(ImportOrganizer.Builder imports) { void writeThrows(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { - if (!exceptions().isEmpty()) { + if (!exceptionTypes().isEmpty()) { writer.write(" throws "); boolean first = true; - for (Type exception : exceptions()) { + for (Type exception : exceptionTypes()) { if (first) { first = false; } else { @@ -76,11 +77,27 @@ void writeBody(ModelWriter writer, ImportOrganizer imports) throws IOException { writer.write("\n"); } - List parameters() { - return parameters; + /** + * List of method parameters. + * + * @return parameters + */ + public List parameters() { + return List.copyOf(parameters); + } + + /** + * List of thrown exceptions. + * + * @return exceptions + */ + public List exceptions() { + return exceptions.stream() + .map(Type::genericTypeName) + .collect(Collectors.toUnmodifiableList()); } - List exceptions() { + List exceptionTypes() { return exceptions; } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java index 611ce2b5c02..e80365f381d 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java @@ -33,12 +33,14 @@ public final class Field extends AnnotatedComponent { private final Content defaultValue; private final boolean isFinal; private final boolean isStatic; + private final boolean isVolatile; private Field(Builder builder) { super(builder); this.defaultValue = builder.defaultValueBuilder.build(); this.isFinal = builder.isFinal; this.isStatic = builder.isStatic; + this.isVolatile = builder.isVolatile; } /** @@ -72,6 +74,9 @@ void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrgani if (isFinal) { writer.write("final "); } + if (isVolatile) { + writer.write("volatile "); + } } type().writeComponent(writer, declaredTokens, imports, classType); writer.write(" "); @@ -92,10 +97,6 @@ void addImports(ImportOrganizer.Builder imports) { defaultValue.addImports(imports); } - boolean isStatic() { - return isStatic; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -125,10 +126,33 @@ public String toString() { return accessModifier().modifierName() + " " + type().fqTypeName() + " " + name(); } - boolean isFinal() { + /** + * Is this field final. + * + * @return whether this is a final field + */ + public boolean isFinal() { return isFinal; } + /** + * Is this field static. + * + * @return whether this is a static field + */ + public boolean isStatic() { + return isStatic; + } + + /** + * Is this field volatile. + * + * @return whether this is a volatile field + */ + public boolean isVolatile() { + return isVolatile; + } + /** * Fluent API builder for {@link Field}. */ @@ -136,6 +160,7 @@ public static final class Builder extends AnnotatedComponent.Builder declaredTokens, ImportOrganizer imports, ClassType classType) - throws IOException { - for (Annotation annotation : annotations()) { - annotation.writeComponent(writer, declaredTokens, imports, classType); - writer.write(" "); - } - type().writeComponent(writer, declaredTokens, imports, classType); - if (vararg) { - writer.write("..."); - } - writer.write(" " + name()); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -83,17 +69,36 @@ public String toString() { return "Parameter{type=" + type().fqTypeName() + ", simpleType=" + type().simpleTypeName() + ", name=" + name() + "}"; } - List description() { + /** + * Description (javadoc lines) of this parameter. + * + * @return parameter description + */ + public List description() { return description; } + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + for (Annotation annotation : annotations()) { + annotation.writeComponent(writer, declaredTokens, imports, classType); + writer.write(" "); + } + type().writeComponent(writer, declaredTokens, imports, classType); + if (vararg) { + writer.write("..."); + } + writer.write(" " + name()); + } + /** * Fluent API builder for {@link Parameter}. */ public static final class Builder extends AnnotatedComponent.Builder { - private boolean vararg = false; private final List description = new ArrayList<>(); + private boolean vararg = false; private Builder() { } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java index 45e24c4100f..3ab5280599e 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java @@ -15,8 +15,8 @@ */ package io.helidon.codegen.classmodel; +import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import io.helidon.common.types.TypeName; @@ -37,15 +37,22 @@ static Type fromTypeName(TypeName typeName) { .type(typeName) .build(); } else if (typeName.wildcard()) { - boolean isObject = typeName.name().equals("?") || Object.class.getName().equals(typeName.name()); - if (isObject) { - return TypeArgument.create("?"); - } else { + List upperBounds = typeName.upperBounds(); + if (upperBounds.isEmpty()) { + if (typeName.lowerBounds().isEmpty()) { + return TypeArgument.create("?"); + } return TypeArgument.builder() .token("?") - .bound(extractBoundTypeName(typeName.genericTypeName())) + .bound(typeName.lowerBounds().getFirst()) + .lowerBound(true) .build(); } + + return TypeArgument.builder() + .token("?") + .bound(upperBounds.getFirst()) + .build(); } return ConcreteType.builder() .type(typeName) @@ -58,24 +65,10 @@ static Type fromTypeName(TypeName typeName) { return typeBuilder.build(); } - private static String extractBoundTypeName(TypeName instance) { - String name = calcName(instance); - StringBuilder nameBuilder = new StringBuilder(name); - - if (!instance.typeArguments().isEmpty()) { - nameBuilder.append('<') - .append(instance.typeArguments() - .stream() - .map(TypeName::resolvedName) - .collect(Collectors.joining(", "))) - .append('>'); - } + abstract TypeName typeName(); - if (instance.array()) { - nameBuilder.append("[]"); - } - - return nameBuilder.toString(); + private static String extractBoundTypeName(TypeName instance) { + return instance.resolvedName(); } private static String calcName(TypeName instance) { diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java index e44e09c6c06..f18cbbfffef 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java @@ -16,10 +16,12 @@ package io.helidon.codegen.classmodel; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import io.helidon.common.types.TypeName; @@ -29,14 +31,16 @@ public final class TypeArgument extends Type implements TypeName { private final TypeName token; - private final Type bound; + private final List bounds; private final List description; + private final boolean isLowerBound; private TypeArgument(Builder builder) { super(builder); this.token = builder.tokenBuilder.build(); - this.bound = builder.bound; + this.bounds = List.copyOf(builder.bounds); this.description = builder.description; + this.isLowerBound = builder.isLowerBound; } /** @@ -65,25 +69,45 @@ public TypeName boxed() { @Override public TypeName genericTypeName() { - if (bound == null) { - return null; + if (bounds.isEmpty()) { + return this; } - return bound.genericTypeName(); + return TypeName.builder() + .from(this) + .typeArguments(List.of()) + .typeParameters(List.of()) + .build(); } @Override void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { writer.write(token.className()); - if (bound != null) { + if (bounds.isEmpty()) { + return; + } + + if (isLowerBound) { + writer.write(" super "); + } else { writer.write(" extends "); - bound.writeComponent(writer, declaredTokens, imports, classType); + } + + if (bounds.size() == 1) { + bounds.getFirst().writeComponent(writer, declaredTokens, imports, classType); + return; + } + for (int i = 0; i < bounds.size(); i++) { + if (i != 0) { + writer.write(" & "); + } + bounds.get(i).writeComponent(writer, declaredTokens, imports, classType); } } @Override void addImports(ImportOrganizer.Builder imports) { - if (bound != null) { + for (Type bound : bounds) { bound.addImports(imports); } } @@ -176,12 +200,25 @@ public List typeParameters() { return List.of(); } + @Override + public List lowerBounds() { + // not yet supported + return List.of(); + } + + @Override + public List upperBounds() { + return bounds.stream() + .map(Type::typeName) + .collect(Collectors.toUnmodifiableList()); + } + @Override public String toString() { - if (bound == null) { + if (bounds.isEmpty()) { return "Token: " + token.className(); } - return "Token: " + token.className() + " Bound: " + bound; + return "Token: " + token.className() + " Bound: " + bounds; } @Override @@ -194,12 +231,12 @@ public boolean equals(Object o) { } TypeArgument typeArgument1 = (TypeArgument) o; return Objects.equals(token, typeArgument1.token) - && Objects.equals(bound, typeArgument1.bound); + && Objects.equals(bounds, typeArgument1.bounds); } @Override public int hashCode() { - return Objects.hash(token, bound); + return Objects.hash(token, bounds); } @Override @@ -207,6 +244,11 @@ public int compareTo(TypeName o) { return token.compareTo(o); } + @Override + TypeName typeName() { + return this; + } + /** * Fluent API builder for {@link TypeArgument}. */ @@ -214,7 +256,9 @@ public static final class Builder extends Type.Builder { private final TypeName.Builder tokenBuilder = TypeName.builder() .generic(true); - private Type bound; + private final List bounds = new ArrayList<>(); + + private boolean isLowerBound; private List description = List.of(); private Builder() { @@ -252,6 +296,18 @@ public Builder bound(Class bound) { return bound(TypeName.create(bound)); } + /** + * Bound is by default an upper bounds (presented as {@code extends} in code). + * By specifying that we use a {@code lowerBound}, the keyword will be {@code super}. + * + * @param lowerBound whether the specified bound is a lower bound (defaults to upper bound); ignore if no bound + * @return updated builder instance + */ + public Builder lowerBound(boolean lowerBound) { + this.isLowerBound = lowerBound; + return this; + } + /** * Type this argument is bound to. * @@ -259,7 +315,18 @@ public Builder bound(Class bound) { * @return updated builder instance */ public Builder bound(TypeName bound) { - this.bound = Type.fromTypeName(bound); + this.bounds.add(Type.fromTypeName(bound)); + return this; + } + + /** + * Type this argument is bound to (may have more than one for intersection types). + * + * @param bound argument bound + * @return updated builder instance + */ + public Builder addBound(TypeName bound) { + this.bounds.add(Type.fromTypeName(bound)); return this; } diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java new file mode 100644 index 00000000000..0e95c163447 --- /dev/null +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Set; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.EnumValue; +import io.helidon.common.types.TypeName; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/* +Test that annotations are correctly written. + */ +class AnnotationTest { + private static final TypeName ANNOTATION_TYPE = TypeName.create(Test.class); + + @Test + void testPrintEnumValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(String.class) + .name("name") + .addAnnotation(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("enumValue", EnumValue.create(enumType, + "ONE")) + .build()) + .build(); + String text = write(field); + + assertThat(text, is(""" + @Test(enumValue = AnnotationTest.TestEnum.ONE) + private String name;""")); + } + + @Test + void testMetaAnnotation() { + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(Annotation.class) + .name("annotation") + .addContentCreate(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("value", "someValue") + .addMetaAnnotation(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("value", "string") + .build()) + .build()) + .build(); + String text = write(field); + + String expected = """ + private Annotation annotation = Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("value", "someValue") + .addMetaAnnotation(Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("value", "string") + .build() + ) + .build();"""; + + assertThat(text, is(expected)); + } + + @Test + void testContentCreateEnumValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(Annotation.class) + .name("annotation") + .addContentCreate(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("enumValue", EnumValue.create(enumType, + "ONE")) + .build()) + .build(); + String text = write(field); + + String expected = """ + private Annotation annotation = Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("enumValue", EnumValue.create(TypeName.create("io.helidon.codegen.classmodel.AnnotationTest.TestEnum"),"ONE")) + .build();"""; + + assertThat(text, is(expected)); + } + + @Test + void testClassValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(String.class) + .name("name") + .addAnnotation(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("classValue", enumType) + .build()) + .build(); + String text = write(field); + + assertThat(text, is(""" + @Test(classValue = AnnotationTest.TestEnum.class) + private String name;""")); + } + + @Test + void testContentCreateClassValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(Annotation.class) + .name("annotation") + .addContentCreate(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("classValue", enumType) + .build()) + .build(); + String text = write(field); + + String expected = """ + private Annotation annotation = Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("classValue", TypeName.create("io.helidon.codegen.classmodel.AnnotationTest.TestEnum")) + .build();"""; + + assertThat(text, is(expected)); + } + + String write(ModelComponent component) { + ImportOrganizer io = ImportOrganizer.builder() + .typeName(AnnotationTest.class.getName()) + .addImport(Test.class) + .addImport(String.class) + .addImport(Annotation.class) + .addImport(TypeName.class) + .addImport(EnumValue.class) + .build(); + StringWriter writer = new StringWriter(); + ModelWriter modelWriter = new ModelWriter(writer, ""); + try { + component.writeComponent(modelWriter, + Set.of(), + io, + ClassType.CLASS); + } catch (IOException e) { + throw new RuntimeException(e); + } + return writer.toString(); + } + + private enum TestEnum { + ONE, + TWO + } +} diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java index e9f763a40a1..fb9ef3c20c2 100644 --- a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java @@ -78,7 +78,7 @@ void testIt() { .putValue("float", 49.0F) .putValue("class", @io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest")) .putValue("type", @io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest")) - .putValue("enum", @java.lang.annotation.ElementType@.FIELD) + .putValue("enum", @io.helidon.common.types.EnumValue@.create(@io.helidon.common.types.TypeName@.create("java.lang.annotation.ElementType"),"FIELD")) .putValue("lstring", @java.util.List@.of("value1","value2")) .putValue("lboolean", @java.util.List@.of(true,false)) .putValue("llong", @java.util.List@.of(49L,50L)) @@ -90,7 +90,7 @@ void testIt() { .putValue("lfloat", @java.util.List@.of(49.0F,50.0F)) .putValue("lclass", @java.util.List@.of(@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"),@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"))) .putValue("ltype", @java.util.List@.of(@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"),@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"))) - .putValue("lenum", @java.util.List@.of(@java.lang.annotation.ElementType@.FIELD,@java.lang.annotation.ElementType@.MODULE)) + .putValue("lenum", @java.util.List@.of(@io.helidon.common.types.EnumValue@.create(@io.helidon.common.types.TypeName@.create("java.lang.annotation.ElementType"),"FIELD"),@io.helidon.common.types.EnumValue@.create(@io.helidon.common.types.TypeName@.create("java.lang.annotation.ElementType"),"MODULE"))) .build()""")); } } diff --git a/codegen/codegen/pom.xml b/codegen/codegen/pom.xml index bdff1303d4a..1d7218a6904 100644 --- a/codegen/codegen/pom.xml +++ b/codegen/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ClassModelFactory.java b/codegen/codegen/src/main/java/io/helidon/codegen/ClassModelFactory.java new file mode 100644 index 00000000000..1135a3df4eb --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ClassModelFactory.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen; + +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.codegen.classmodel.Annotation; +import io.helidon.codegen.classmodel.ClassBase; +import io.helidon.codegen.classmodel.Constructor; +import io.helidon.codegen.classmodel.Executable; +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.classmodel.Parameter; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Transforms class model to a {@link io.helidon.common.types.TypeInfo}. + */ +final class ClassModelFactory { + private ClassModelFactory() { + } + + static TypeInfo create(RoundContext ctx, + TypeName requestedTypeName, + ClassBase requestedType) { + + var builder = TypeInfo.builder() + .typeName(requestedTypeName) + .kind(requestedType.kind()) + .accessModifier(requestedType.accessModifier()) + .description(String.join("\n", requestedType.description())); + + for (Annotation annotation : requestedType.annotations()) { + builder.addAnnotation(annotation.toTypesAnnotation()); + } + + requestedType.superTypeName() + .flatMap(ctx::typeInfo) + .ifPresent(builder::superTypeInfo); + + List typeNames = requestedType.interfaceTypeNames(); + for (TypeName typeName : typeNames) { + ctx.typeInfo(typeName).ifPresent(builder::addInterfaceTypeInfo); + } + for (Field field : requestedType.fields()) { + addField(builder, field); + } + for (Constructor constructor : requestedType.constructors()) { + addConstructor(requestedTypeName, builder, constructor); + } + for (Method method : requestedType.methods()) { + addMethod(builder, method); + } + + for (ClassBase innerClass : requestedType.innerClasses()) { + addInnerClass(requestedTypeName, builder, innerClass); + } + + return builder.build(); + } + + private static void addInnerClass(TypeName requestedTypeName, TypeInfo.Builder builder, ClassBase innerClass) { + + builder.addElementInfo(innerInfo -> innerInfo + .typeName(innerClassTypeName(requestedTypeName, innerClass.name())) + .kind(ElementKind.CLASS) + .elementName(innerClass.name()) + .accessModifier(innerClass.accessModifier()) + .update(it -> { + if (innerClass.isStatic()) { + it.addElementModifier(Modifier.STATIC); + } + if (innerClass.isAbstract()) { + it.addElementModifier(Modifier.ABSTRACT); + } + if (innerClass.isFinal()) { + it.addElementModifier(Modifier.FINAL); + } + }) + .description(String.join("\n", innerClass.description())) + .update(it -> addAnnotations(it, innerClass.annotations())) + ); + } + + private static TypeName innerClassTypeName(TypeName requestedTypeName, String name) { + return TypeName.builder(requestedTypeName) + .addEnclosingName(requestedTypeName.className()) + .className(name) + .build(); + } + + private static void addMethod(TypeInfo.Builder builder, Method method) { + builder.addElementInfo(methodInfo -> methodInfo + .kind(ElementKind.METHOD) + .elementName(method.name()) + .accessModifier(method.accessModifier()) + .update(it -> { + if (method.isStatic()) { + it.addElementModifier(Modifier.STATIC); + } + if (method.isFinal()) { + it.addElementModifier(Modifier.FINAL); + } + if (method.isAbstract()) { + it.addElementModifier(Modifier.ABSTRACT); + } + if (method.isDefault()) { + it.addElementModifier(Modifier.DEFAULT); + } + }) + .description(String.join("\n", method.description())) + .update(it -> addAnnotations(it, method.annotations())) + .update(it -> processExecutable(it, method)) + .typeName(method.typeName()) + ); + } + + private static void processExecutable(TypedElementInfo.Builder builder, Executable executable) { + for (Parameter parameter : executable.parameters()) { + builder.addParameterArgument(arg -> arg + .kind(ElementKind.PARAMETER) + .elementName(parameter.name()) + .typeName(parameter.typeName()) + .description(String.join("\n", parameter.description())) + .update(it -> addAnnotations(it, parameter.annotations())) + ); + } + builder.addThrowsChecked(executable.exceptions() + .stream() + .collect(Collectors.toUnmodifiableSet())); + } + + private static void addConstructor(TypeName typeName, TypeInfo.Builder builder, Constructor constructor) { + builder.addElementInfo(ctrInfo -> ctrInfo + .typeName(typeName) + .kind(ElementKind.CONSTRUCTOR) + .accessModifier(constructor.accessModifier()) + .description(String.join("\n", constructor.description())) + .update(it -> addAnnotations(it, constructor.annotations())) + .update(it -> processExecutable(it, constructor)) + ); + } + + private static void addField(TypeInfo.Builder builder, Field field) { + builder.addElementInfo(fieldInfo -> fieldInfo + .typeName(field.typeName()) + .kind(ElementKind.FIELD) + .accessModifier(field.accessModifier()) + .elementName(field.name()) + .description(String.join("\n", field.description())) + .update(it -> addAnnotations(it, field.annotations())) + .update(it -> { + if (field.isStatic()) { + it.addElementModifier(Modifier.STATIC); + } + if (field.isFinal()) { + it.addElementModifier(Modifier.FINAL); + } + if (field.isVolatile()) { + it.addElementModifier(Modifier.VOLATILE); + } + }) + ); + } + + private static void addAnnotations(TypedElementInfo.Builder element, List annotations) { + for (Annotation annotation : annotations) { + element.addAnnotation(annotation.toTypesAnnotation()); + } + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java b/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java index 844149801b6..bad63f59bf3 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java @@ -20,9 +20,9 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; import java.util.function.Predicate; @@ -35,7 +35,6 @@ import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementInfo; /** * Central piece of code processing and generation. @@ -60,52 +59,48 @@ public class Codegen { SUPPORTED_APT_OPTIONS = Set.copyOf(supportedOptions); } - private final Map> typeToExtensions = new HashMap<>(); - private final Map> extensionPredicates = new IdentityHashMap<>(); private final CodegenContext ctx; - private final List extensions; + private final List extensions; private final Set supportedAnnotations; + private final Set supportedMetaAnnotations; private final Set supportedPackagePrefixes; private Codegen(CodegenContext ctx, TypeName generator) { this.ctx = ctx; + Set supportedAnnotations = new HashSet<>(ctx.mapperSupportedAnnotations()); + Set supportedMetaAnnotations = new HashSet<>(); + Set supportedPackagePrefixes = new HashSet<>(); + this.extensions = EXTENSIONS.stream() .map(it -> { CodegenExtension extension = it.create(this.ctx, generator); - for (TypeName typeName : it.supportedAnnotations()) { - typeToExtensions.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(extension); - } - Collection packages = it.supportedAnnotationPackages(); - if (!packages.isEmpty()) { - extensionPredicates.put(extension, discoveryPredicate(packages)); - } + Set extensionAnnotations = it.supportedAnnotations(); + Set extensionPackages = it.supportedAnnotationPackages(); + Set extensionMetaAnnotations = it.supportedMetaAnnotations(); - return extension; - }) - .toList(); + supportedAnnotations.addAll(extensionAnnotations); + supportedMetaAnnotations.addAll(extensionMetaAnnotations); + supportedPackagePrefixes.addAll(extensionPackages); - // handle supported annotations and package prefixes - Set packagePrefixes = new HashSet<>(); - Set annotations = new HashSet<>(ctx.mapperSupportedAnnotations()); + Predicate annotationPredicate = discoveryPredicate(extensionAnnotations, + extensionPackages); - for (CodegenExtensionProvider extension : EXTENSIONS) { - annotations.addAll(extension.supportedAnnotations()); + return new ExtensionInfo(extension, + annotationPredicate, + extensionMetaAnnotations); + }) + .toList(); - ctx.mapperSupportedAnnotationPackages() - .stream() - .map(Codegen::toPackagePrefix) - .forEach(packagePrefixes::add); - } ctx.mapperSupportedAnnotationPackages() .stream() .map(Codegen::toPackagePrefix) - .forEach(packagePrefixes::add); + .forEach(supportedPackagePrefixes::add); - this.supportedAnnotations = Set.copyOf(annotations); - this.supportedPackagePrefixes = Set.copyOf(packagePrefixes); + this.supportedAnnotations = Set.copyOf(supportedAnnotations); + this.supportedPackagePrefixes = Set.copyOf(supportedPackagePrefixes); + this.supportedMetaAnnotations = Set.copyOf(supportedMetaAnnotations); } /** @@ -146,12 +141,11 @@ public void process(List allTypes) { // type info list will contain all mapped annotations, so this is the state we can do annotation processing on List annotatedTypes = annotatedTypes(allTypes); - for (CodegenExtension extension : extensions) { + for (var extension : extensions) { // and now for each extension, we discover types that contain annotations supported by that extension - // and create a new round context for each extension - - RoundContextImpl roundCtx = createRoundContext(annotatedTypes, extension); - extension.process(roundCtx); + // and create a new round context + RoundContextImpl roundCtx = createRoundContext(annotatedTypes, extension, toWrite); + extension.extension().process(roundCtx); toWrite.addAll(roundCtx.newTypes()); } @@ -165,9 +159,9 @@ public void processingOver() { List toWrite = new ArrayList<>(); // do processing over in each extension - for (CodegenExtension extension : extensions) { - RoundContextImpl roundCtx = createRoundContext(List.of(), extension); - extension.processingOver(roundCtx); + for (var extension : extensions) { + RoundContextImpl roundCtx = createRoundContext(List.of(), extension, toWrite); + extension.extension().processingOver(roundCtx); toWrite.addAll(roundCtx.newTypes()); } @@ -193,11 +187,25 @@ public Set supportedAnnotationPackagePrefixes() { return supportedPackagePrefixes; } - private static Predicate discoveryPredicate(Collection packages) { - List prefixes = packages.stream() + /** + * A set of annotation types that may annotate annotation types. + * + * @return set of meta annotations for annotations to be processed + */ + public Set supportedMetaAnnotations() { + return supportedMetaAnnotations; + } + + private static Predicate discoveryPredicate(Set extensionAnnotations, + Collection extensionPackages) { + List prefixes = extensionPackages.stream() .map(it -> it.endsWith(".*") ? it.substring(0, it.length() - 2) : it) .toList(); + return typeName -> { + if (extensionAnnotations.contains(typeName)) { + return true; + } String packageName = typeName.packageName(); for (String prefix : prefixes) { if (packageName.startsWith(prefix)) { @@ -222,7 +230,7 @@ private List annotatedTypes(List allTypes) { List result = new ArrayList<>(); for (TypeInfo typeInfo : allTypes) { - result.add(new TypeInfoAndAnnotations(typeInfo, annotations(typeInfo))); + result.add(new TypeInfoAndAnnotations(typeInfo, TypeHierarchy.nestedAnnotations(ctx, typeInfo))); } return result; } @@ -238,74 +246,65 @@ private void writeNewTypes(List toWrite) { } } - private RoundContextImpl createRoundContext(List annotatedTypes, CodegenExtension extension) { - Set extAnnots = new HashSet<>(); - Map> extAnnotToType = new HashMap<>(); - Map extTypes = new HashMap<>(); + private RoundContextImpl createRoundContext(List annotatedTypes, + ExtensionInfo extension, + List newTypes) { + Set availableAnnotations = new HashSet<>(); + Map> annotationToTypes = new HashMap<>(); + Map processedTypes = new HashMap<>(); + Map> metaAnnotationToAnnotations = new HashMap<>(); + // now go through all available annotated types and make sure we only include the ones required by this extension for (TypeInfoAndAnnotations annotatedType : annotatedTypes) { - for (TypeName typeName : annotatedType.annotations()) { - boolean added = false; - List validExts = this.typeToExtensions.get(typeName); - if (validExts != null) { - for (CodegenExtension validExt : validExts) { - if (validExt == extension) { - extAnnots.add(typeName); - extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(annotatedType.typeInfo()); - extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); - added = true; - } - } - } - if (!added) { - Predicate predicate = this.extensionPredicates.get(extension); - if (predicate != null && predicate.test(typeName)) { - extAnnots.add(typeName); - extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(annotatedType.typeInfo()); - extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); - } + for (TypeName annotationType : annotatedType.annotations()) { + boolean metaAnnotated = metaAnnotations(extension, metaAnnotationToAnnotations, annotationType); + if (metaAnnotated || extension.supportedAnnotationsPredicate().test(annotationType)) { + availableAnnotations.add(annotationType); + processedTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo()); + annotationToTypes.computeIfAbsent(annotationType, k -> new ArrayList<>()) + .add(annotatedType.typeInfo()); + // annotation is meta-annotated with a supported meta-annotation, + // or we support the annotation type, or it is prefixed by the package prefix } } } return new RoundContextImpl( - Set.copyOf(extAnnots), - Map.copyOf(extAnnotToType), - List.copyOf(extTypes.values())); + ctx, + newTypes, + Set.copyOf(availableAnnotations), + Map.copyOf(annotationToTypes), + Map.copyOf(metaAnnotationToAnnotations), + List.copyOf(processedTypes.values())); } - private Set annotations(TypeInfo theTypeInfo) { - Set result = new HashSet<>(); - - // on type - theTypeInfo.annotations() - .stream() - .map(Annotation::typeName) - .forEach(result::add); - - // on fields, methods etc. - theTypeInfo.elementInfo() - .stream() - .map(TypedElementInfo::annotations) - .flatMap(List::stream) - .map(Annotation::typeName) - .forEach(result::add); - - // on parameters - theTypeInfo.elementInfo() - .stream() - .map(TypedElementInfo::parameterArguments) - .flatMap(List::stream) - .map(TypedElementInfo::annotations) - .flatMap(List::stream) - .map(Annotation::typeName) - .forEach(result::add); - - return result; + private boolean metaAnnotations(ExtensionInfo extension, + Map> metaAnnotationToAnnotations, + TypeName annotationType) { + Optional annotationInfo = ctx.typeInfo(annotationType); + if (annotationInfo.isEmpty()) { + return false; + } + TypeInfo annotationTypeInfo = annotationInfo.get(); + + boolean metaAnnotated = false; + for (TypeName metaAnnotation : extension.supportedMetaAnnotations()) { + for (Annotation anAnnotation : annotationTypeInfo.allAnnotations()) { + if (anAnnotation.typeName().equals(metaAnnotation)) { + metaAnnotated = true; + metaAnnotationToAnnotations.computeIfAbsent(metaAnnotation, k -> new HashSet<>()) + .add(annotationType); + } + } + } + return metaAnnotated; } private record TypeInfoAndAnnotations(TypeInfo typeInfo, Set annotations) { } + + private record ExtensionInfo(CodegenExtension extension, + Predicate supportedAnnotationsPredicate, + Set supportedMetaAnnotations) { + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java index 5a4b2c80579..0133d82af47 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java @@ -66,7 +66,8 @@ default Optional moduleName() { CodegenLogger logger(); /** - * Current code generation scope. Usually guessed from the environment, can be overridden using {@link CodegenOptions#CODEGEN_SCOPE} + * Current code generation scope. Usually guessed from the environment, can be overridden using + * {@link CodegenOptions#CODEGEN_SCOPE} * * @return scope */ @@ -80,10 +81,12 @@ default Optional moduleName() { CodegenOptions options(); /** - * Discover information about the provided type. + * Discover information about the provided type. This method only checks existing classes in the + * system, and ignored classes created as part of the current processing round. * * @param typeName type name to discover * @return discovered type information, or empty if the type cannot be discovered + * @see io.helidon.codegen.RoundContext#typeInfo(io.helidon.common.types.TypeName) */ Optional typeInfo(TypeName typeName); @@ -144,4 +147,13 @@ default Optional moduleName() { * @return set of supported options */ Set> supportedOptions(); + + /** + * Get the unique name for the element within the provided type. + * + * @param type type that owns the element + * @param element the element + * @return unique name for the element (will always start with the element name) + */ + String uniqueName(TypeInfo type, TypedElementInfo element); } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java index 711644ca5a8..19856a969ca 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java @@ -16,8 +16,10 @@ package io.helidon.codegen; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -29,12 +31,17 @@ import io.helidon.codegen.spi.TypeMapper; import io.helidon.codegen.spi.TypeMapperProvider; import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.ElementSignature; +import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; /** * Base of codegen context implementation taking care of the common parts of the API. */ public abstract class CodegenContextBase implements CodegenContext { + // class -> method name -> element signature + private final Map> uniqueNames = new HashMap<>(); private final List elementMappers; private final List typeMappers; private final List annotationMappers; @@ -149,6 +156,13 @@ public CodegenOptions options() { return options; } + @Override + public String uniqueName(TypeInfo type, TypedElementInfo element) { + return uniqueNames.computeIfAbsent(type.typeName(), it -> new HashMap<>()) + .computeIfAbsent(element.elementName(), ElementSignatures::new) + .uniqueName(element.signature()); + } + private static void addSupported(CodegenProvider provider, Set> supportedOptions, Set supportedPackages, @@ -160,4 +174,23 @@ private static void addSupported(CodegenProvider provider, .map(it -> it.endsWith(".*") ? it : it + ".*") .forEach(supportedPackages::add); } + + private static class ElementSignatures { + private final Map names = new HashMap<>(); + private final String name; + + private ElementSignatures(String name) { + this.name = name; + } + + public String uniqueName(ElementSignature signature) { + int size = names.size(); + if (names.containsKey(signature)) { + return names.get(signature); + } + String nextName = size == 0 ? name : name + "_" + size; + names.put(signature, nextName); + return nextName; + } + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java index e2055d1e816..afaf4a72e4b 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java @@ -107,4 +107,9 @@ public Set mapperSupportedAnnotationPackages() { public Set> supportedOptions() { return delegate.supportedOptions(); } + + @Override + public String uniqueName(TypeInfo type, TypedElementInfo element) { + return delegate.uniqueName(type, element); + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java index f4b2ab46ac2..abd03ec7392 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java @@ -37,11 +37,21 @@ public interface CodegenFiler { * * @param classModel class model to write out * @param originatingElements elements that caused this type to be generated - * (you can use {@link io.helidon.common.types.TypeInfo#originatingElement()} for example + * (you can use {@link io.helidon.common.types.TypeInfo#originatingElementValue()}) * @return written path, we expect to always run on local file system */ Path writeSourceFile(ClassModel classModel, Object... originatingElements); + /** + * Write a source file using string content. + * + * @param type type of the file to generate + * @param content source code to write + * @param originatingElements elements that caused this type to be generated + * @return written path, we expect to always run on local file system + */ + Path writeSourceFile(TypeName type, String content, Object... originatingElements); + /** * Write a resource file. * @@ -64,6 +74,18 @@ default FilerTextResource textResource(String location, Object... originatingEle throw new UnsupportedOperationException("Method textResource not implemented yet on " + getClass().getName()); } + /** + * A text resource that can be updated in the output. + * Note that the resource can only be written once per processing round. + * + * @param location location to read/write to in the classes output directory + * @param originatingElements elements that caused this file to be generated + * @return the resource that can be used to update the file + */ + default FilerResource resource(String location, Object... originatingElements) { + throw new UnsupportedOperationException("Method resource not implemented yet on " + getClass().getName()); + } + /** * Write a {@code META-INF/services} file for a specific provider interface and implementation(s). * diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java new file mode 100644 index 00000000000..e923e95a61d --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen; + +import java.net.URI; +import java.time.Duration; + +import io.helidon.common.Size; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Validation utilities. + */ +public final class CodegenValidator { + private CodegenValidator() { + } + + /** + * Validate a URI value in an annotation. + * + * @param enclosingType type that owns the element + * @param element annotated element + * @param annotationType type of annotation + * @param property property of annotation + * @param value actual value read from the annotation property + * @return the value + * @throws io.helidon.codegen.CodegenException with correct source element describing the problem + */ + public static String validateUri(TypeName enclosingType, + TypedElementInfo element, + TypeName annotationType, + String property, + String value) { + try { + URI.create(value); + return value; + } catch (Exception e) { + throw new CodegenException("URI expression of annotation " + annotationType.fqName() + "." + + property + "(): " + + "\"" + value + "\" cannot be parsed. Invalid URI.", + e, + element.originatingElementValue()); + } + } + + /** + * Validate a duration annotation on a method, field, or constructor. + * + * @param enclosingType type that owns the element + * @param element annotated element + * @param annotationType type of annotation + * @param property property of annotation + * @param value actual value read from the annotation property + * @return the value + * @throws io.helidon.codegen.CodegenException with correct source element describing the problem + */ + public static String validateDuration(TypeName enclosingType, + TypedElementInfo element, + TypeName annotationType, + String property, + String value) { + try { + Duration.parse(value); + return value; + } catch (Exception e) { + throw new CodegenException("Duration expression of annotation " + annotationType.fqName() + "." + + property + "(): " + + "\"" + value + "\" cannot be parsed. Duration expects an" + + " expression such as 'PT1S' (1 second), 'PT0.1S' (tenth of a second)." + + " Please check javadoc of " + Duration.class.getName() + " class.", + e, + element.originatingElementValue()); + } + } + + /** + * Validate a {@link io.helidon.common.Size} annotation on a method, field, or constructor. + * + * @param enclosingType type that owns the element + * @param element annotated element + * @param annotationType type of annotation + * @param property property of annotation + * @param value actual value read from the annotation property + * @return the value + * @throws io.helidon.codegen.CodegenException with correct source element describing the problem + */ + public static String validateSize(TypeName enclosingType, + TypedElementInfo element, + TypeName annotationType, + String property, + String value) { + try { + Size.parse(value); + return value; + } catch (Exception e) { + throw new CodegenException("Size expression of annotation " + annotationType.fqName() + "." + + property + "(): " + + "\"" + value + "\" cannot be parsed. Size expects an" + + " expression such as '120 KB' (120 * 1024 * 1024), " + + "'120 kB' (120 * 1000 * 1000), or '120 KiB' (same as KB)" + + " Please check javadoc of " + Size.class.getName() + " class.", + e, + element.originatingElementValue()); + } + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java b/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java index 007f272f325..cfb5bfd1119 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java @@ -48,6 +48,16 @@ public static boolean isMethod(TypedElementInfo element) { return ElementKind.METHOD == element.kind(); } + /** + * Predicate for constructor element kind. + * + * @param element typed element info to test + * @return whether the element represents a constructor + */ + public static boolean isConstructor(TypedElementInfo element) { + return ElementKind.CONSTRUCTOR == element.kind(); + } + /** * Predicate for field element kind. * @@ -68,6 +78,16 @@ public static boolean isStatic(TypedElementInfo element) { return element.elementModifiers().contains(Modifier.STATIC); } + /** + * Predicate for abstract modifier. + * + * @param element typed element info to test + * @return whether the element has abstract modifier + */ + public static boolean isAbstract(TypedElementInfo element) { + return element.elementModifiers().contains(Modifier.ABSTRACT); + } + /** * Predicate for private modifier. * diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/FilerResource.java b/codegen/codegen/src/main/java/io/helidon/codegen/FilerResource.java new file mode 100644 index 00000000000..45d19079bdc --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/FilerResource.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen; + +/** + * A resource from output (such as {@code target/META-INF/helidon}) that can have existing + * values, and may be replaced with a new value. + */ +public interface FilerResource { + /** + * Existing bytes of the resource. Returns empty array, if the resource does not exist or is empty. + * + * @return bytes of the resource + */ + byte[] bytes(); + + /** + * New bytes of the resource. + * + * @param newBytes new bytes to {@link #write()} to the resource file + */ + void bytes(byte[] newBytes); + + /** + * Writes the new bytes to the output. This operation can only be called once per codegen round. + */ + void write(); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java index 1cf876ec26e..1533e55b7be 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java @@ -77,4 +77,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(name); } + + @Override + public String toString() { + return name + "(" + defaultValue + ")"; + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java index 89b240254d0..34ed3dcf3ff 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.Optional; +import java.util.Set; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.common.types.TypeInfo; @@ -44,13 +45,25 @@ public interface RoundContext { Collection types(); /** - * All types annotated with a specific annotation. + * All types annotated with a specific annotation (including types that inherit such annotation from super types or + * through interfaces). * * @param annotationType annotation to check * @return types that contain the annotation */ Collection annotatedTypes(TypeName annotationType); + /** + * Annotation types present on wanted types, annotated with the specific "meta" annotation. + * + * @param metaAnnotation annotations annotated with the provided annotation + * @return annotation types + */ + default Collection annotatedAnnotations(TypeName metaAnnotation) { + // default implementation for backward compatibility reasons + return Set.of(); + } + /** * All elements annotated with a specific annotation. * @@ -80,9 +93,24 @@ public interface RoundContext { * annotations. * Whether another extension was already called depends on its {@link io.helidon.codegen.spi.CodegenExtensionProvider} * weight. + * This method will return top level class model builder if the type represents an inner class of it. * * @param type type of the generated type * @return class model of the new type if any */ Optional generatedType(TypeName type); + + /** + * Discover information about the provided type. + *

    + * In case the type was generated by this processing round (even in another extension), + * the type info will reflect the current state of the + * class model builder that was registered with + * {@link io.helidon.codegen.RoundContext#addGeneratedType(io.helidon.common.types.TypeName, + * io.helidon.codegen.classmodel.ClassModel.Builder, io.helidon.common.types.TypeName, Object...)}. + * + * @param typeName type name to discover + * @return discovered type information, or empty if the type cannot be discovered + */ + Optional typeInfo(TypeName typeName); } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java index e19ca8a735e..4169b1ea0ba 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.Set; +import io.helidon.codegen.classmodel.ClassBase; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; @@ -32,15 +33,23 @@ class RoundContextImpl implements RoundContext { private final Map newTypes = new HashMap<>(); private final Map> annotationToTypes; + private final Map> metaAnnotated; private final List types; + private final CodegenContext ctx; + private final List newTypesFromPreviousExtensions; private final Collection annotations; - RoundContextImpl(Set annotations, + RoundContextImpl(CodegenContext ctx, + List newTypes, + Set annotations, Map> annotationToTypes, + Map> metaAnnotated, List types) { - + this.ctx = ctx; + this.newTypesFromPreviousExtensions = newTypes; this.annotations = annotations; this.annotationToTypes = annotationToTypes; + this.metaAnnotated = metaAnnotated; this.types = types; } @@ -83,7 +92,9 @@ public Collection annotatedTypes(TypeName annotationType) { List result = new ArrayList<>(); for (TypeInfo typeInfo : typeInfos) { - if (typeInfo.hasAnnotation(annotationType)) { + if (typeInfo.hasAnnotation(annotationType) || TypeHierarchy.hierarchyAnnotations(ctx, typeInfo) + .stream() + .anyMatch(it -> it.typeName().equals(annotationType))) { result.add(typeInfo); } } @@ -91,6 +102,11 @@ public Collection annotatedTypes(TypeName annotationType) { return result; } + @Override + public Collection annotatedAnnotations(TypeName metaAnnotation) { + return Optional.ofNullable(metaAnnotated.get(metaAnnotation)).orElseGet(Set::of); + } + @Override public void addGeneratedType(TypeName type, ClassModel.Builder newClass, @@ -104,6 +120,37 @@ public Optional generatedType(TypeName type) { return Optional.ofNullable(newTypes.get(type)).map(ClassCode::classModel); } + @Override + public Optional typeInfo(TypeName typeName) { + var found = ctx.typeInfo(typeName); + if (found.isPresent()) { + return found; + } + + return generatedClass(typeName) + .map(it -> ClassModelFactory.create( + this, + typeName, + it)); + } + + private Optional generatedClass(TypeName typeName) { + for (ClassCode classCode : newTypes.values()) { + Optional inProgress = classCode.classModel().find(typeName); + if (inProgress.isPresent()) { + return inProgress; + } + } + for (ClassCode classCode : newTypesFromPreviousExtensions) { + Optional inProgress = classCode.classModel().find(typeName); + if (inProgress.isPresent()) { + return inProgress; + } + } + + return Optional.empty(); + } + Collection newTypes() { return newTypes.values(); } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java index 67cedf9f803..f110ecd820d 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java @@ -87,6 +87,11 @@ public int hashCode() { return Objects.hash(name); } + @Override + public String toString() { + return name + "(" + defaultValue + ")"; + } + private Set toSet(String[] strings) { return Stream.of(strings) .map(String::trim) diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/TypeHierarchy.java b/codegen/codegen/src/main/java/io/helidon/codegen/TypeHierarchy.java new file mode 100644 index 00000000000..0afbd359520 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/TypeHierarchy.java @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.common.types.TypeNames.INHERITED; +import static java.util.function.Predicate.not; + +/** + * Utilities for type hierarchy. + */ +public final class TypeHierarchy { + private TypeHierarchy() { + } + + /** + * Find all annotations on the whole type hierarchy. + * Adds all annotations on the provided type, and all + * {@link java.lang.annotation.Inherited} annotations on supertype(s) and/or interface(s). + * + * @param ctx codegen context + * @param type type info to process + * @return all annotations on the type and in its hierarchy + */ + public static List hierarchyAnnotations(CodegenContext ctx, TypeInfo type) { + Map annotations = new LinkedHashMap<>(); + + // this type + type.annotations().forEach(annot -> annotations.put(annot.typeName(), annot)); + // inherited from supertype + type.superTypeInfo().ifPresent(it -> { + it.inheritedAnnotations() + .forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + }); + // and from interfaces (in order of implementation) + for (TypeInfo typeInfo : type.interfaceTypeInfo()) { + typeInfo.annotations() + .stream() + .filter(annot -> typeInfo.hasMetaAnnotation(annot.typeName(), INHERITED)) + .forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + } + + Set processedTypes = new HashSet<>(annotations.keySet()); + + // now we have a full list of annotations that are explicitly written in sources, now collect meta-annotations + // i.e. all annotations on the annotations we have that have @Inherited placed on them + processMetaAnnotations(ctx, processedTypes, annotations); + + return List.copyOf(annotations.values()); + } + + /** + * Find all annotations on the whole type hierarchy. + * Adds all annotations on the provided element, and all + * {@link java.lang.annotation.Inherited} annotations on the same element from supertype(s), + * and/or interfaces, and/or annotations. + *

    + * Based on element type: + *

      + *
    • Constructor: only uses annotations from the current element
    • + *
    • Constructor parameter: ditto
    • + *
    • Method: uses annotations from the current element, and from the overridden method/interface method
    • + *
    • Method parameter: use + * {@link #hierarchyAnnotations(CodegenContext, + * io.helidon.common.types.TypeInfo, + * io.helidon.common.types.TypedElementInfo, + * io.helidon.common.types.TypedElementInfo, + * int)} instead
    • + *
    • Field: only uses annotations from the current element
    • + *
    + * If the same annotation is on multiple levels (i.e. method, super type method, and interface), it will always be used + * ONLY from the "closest" type - order is: this element, super type element, interface element. + * + * @param ctx codegen context + * @param type type info owning the executable + * @param element executable (method or constructor) element info + * @return all annotations on the type and in its hierarchy + */ + public static List hierarchyAnnotations(CodegenContext ctx, TypeInfo type, TypedElementInfo element) { + if (element.kind() != ElementKind.METHOD) { + return element.annotations(); + } + + // find the same method on supertype/interfaces + List prototypes = new ArrayList<>(); + Set processedTypes = new HashSet<>(); + String packageName = type.typeName().packageName(); + // extends + type.superTypeInfo().ifPresent(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + element, + packageName)); + // implements + type.interfaceTypeInfo().forEach(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + element, + packageName)); + + // we have collected all methods in the hierarchy, let's collect their annotations + Map annotations = new LinkedHashMap<>(); + // this type + element.annotations().forEach(annot -> annotations.put(annot.typeName(), annot)); + // inherited from supertype(s) and interface(s) + for (TypedElementInfo prototype : prototypes) { + prototype.annotations().forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + } + + // now we have a full list of annotations that are explicitly written in sources, now collect meta-annotations + // i.e. all annotations on the annotations we have that have @Inherited placed on them + processMetaAnnotations(ctx, processedTypes, annotations); + + return List.copyOf(annotations.values()); + } + + /** + * Annotations of a parameter, taken from the full inheritance hierarchy (super type(s), interface(s). + * + * @param ctx codegen context to obtain {@link io.helidon.common.types.TypeInfo} of types + * @param type type info of the processed type + * @param executable owner of the parameter (constructor or method) + * @param parameter parameter info itself + * @param parameterIndex index of the parameter within the method (as names may be wrong at runtime) + * @return list of annotations on this parameter on this type, super type(s), and interface methods it implements + */ + public static List hierarchyAnnotations(CodegenContext ctx, + TypeInfo type, + TypedElementInfo executable, + TypedElementInfo parameter, + int parameterIndex) { + if (parameter.kind() != ElementKind.PARAMETER) { + throw new CodegenException("This method only supports processing of parameter, yet kind is: " + parameter.kind()); + } + if (!(executable.kind() == ElementKind.CONSTRUCTOR || executable.kind() == ElementKind.METHOD)) { + throw new CodegenException("This method only supports processing of parameters of methods or constructors, yet " + + "executable kind is: " + executable.kind()); + } + if (executable.kind() == ElementKind.CONSTRUCTOR) { + // constructor parameters are not inherited + return parameter.annotations(); + } + + // find the same method on supertype/interfaces + List prototypes = new ArrayList<>(); + Set processedTypes = new HashSet<>(); + String packageName = type.typeName().packageName(); + // extends + type.superTypeInfo().ifPresent(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + executable, + packageName)); + // implements + type.interfaceTypeInfo().forEach(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + executable, + packageName)); + + // we have collected all methods in the hierarchy, let's collect their annotations + Map annotations = new LinkedHashMap<>(); + // this type + parameter.annotations().forEach(annot -> annotations.put(annot.typeName(), annot)); + // inherited from supertype(s) and interface(s) + for (TypedElementInfo prototype : prototypes) { + prototype.parameterArguments() + .get(parameterIndex) + .annotations() + .forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + } + + // now we have a full list of annotations that are explicitly written in sources, now collect meta-annotations + // i.e. all annotations on the annotations we have that have @Inherited placed on them + processMetaAnnotations(ctx, processedTypes, annotations); + + return List.copyOf(annotations.values()); + + } + + /** + * Annotations on the {@code typeInfo}, it's methods, and method parameters. + * + * @param ctx context + * @param typeInfo type info to check + * @return a set of all annotation types on any of the elements, including inherited annotations + */ + public static Set nestedAnnotations(CodegenContext ctx, TypeInfo typeInfo) { + Set result = new HashSet<>(); + + // on type + typeInfo.annotations() + .stream() + .map(Annotation::typeName) + .forEach(result::add); + typeInfo.inheritedAnnotations() + .stream() + .map(Annotation::typeName) + .forEach(result::add); + + // on fields, methods etc. + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::annotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::inheritedAnnotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + // on parameters + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::parameterArguments) + .flatMap(List::stream) + .map(TypedElementInfo::annotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::parameterArguments) + .flatMap(List::stream) + .map(TypedElementInfo::inheritedAnnotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + return result; + /* + Set result = new HashSet<>(); + + // on type + hierarchyAnnotations(ctx, typeInfo) + .stream() + .map(Annotation::typeName) + .forEach(result::add); + + // on fields, methods etc. + typeInfo.elementInfo() + .stream() + .map(it -> hierarchyAnnotations(ctx, typeInfo, it)) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + // on parameters + typeInfo.elementInfo() + .stream() + .forEach(it -> { + int index = 0; + for (var param : it.parameterArguments()) { + hierarchyAnnotations(ctx, typeInfo, it, param, index++) + .stream() + .map(Annotation::typeName) + .forEach(result::add); + } + }); + + return result; + */ + } + + private static void processMetaAnnotations(CodegenContext ctx, + Set processedTypes, + Map annotations) { + + List newAnnotations = new ArrayList<>(); + + for (Annotation value : annotations.values()) { + Optional typeInfo = ctx.typeInfo(value.typeName()); + if (typeInfo.isPresent()) { + // we can handle only annotations on classpath, all others are just ignored + TypeInfo annotationInfo = typeInfo.get(); + + annotationInfo + .annotations() + .forEach(metaAnnotation -> { + collectMetaAnnotations(ctx, processedTypes, newAnnotations, metaAnnotation); + }); + } + } + + newAnnotations.forEach(it -> annotations.putIfAbsent(it.typeName(), it)); + } + + private static void collectMetaAnnotations(CodegenContext ctx, + Set processedTypes, + List metaAnnotations, + Annotation annotation) { + if (!processedTypes.add(annotation.typeName())) { + // this annotation was already processed + return; + } + Optional typeInfo = ctx.typeInfo(annotation.typeName()); + if (typeInfo.isEmpty()) { + return; + } + TypeInfo annotationInfo = typeInfo.get(); + if (annotationInfo.hasAnnotation(INHERITED)) { + metaAnnotations.add(annotation); + } + // and check all annotations of this annotation + annotationInfo.annotations() + .forEach(metaAnnotation -> collectMetaAnnotations(ctx, processedTypes, metaAnnotations, metaAnnotation)); + } + + private static void collectInheritedMethods(Set processed, + List collected, + TypeInfo type, + TypedElementInfo method, + String currentPackage) { + if (!processed.add(type.typeName())) { + // already handled this type + return; + } + + inherited( + type, + method, + method.parameterArguments() + .stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()), + currentPackage) + .ifPresent(collected::add); + + type.superTypeInfo().ifPresent(it -> collectInheritedMethods(processed, collected, it, method, currentPackage)); + for (TypeInfo typeInfo : type.interfaceTypeInfo()) { + collectInheritedMethods(processed, collected, typeInfo, method, currentPackage); + } + } + + /** + * Check if the provided type declares a method that is overridden. + * + * @param type first immediate supertype we will be checking + * @param method method we are investigating + * @param arguments method signature + * @param currentPackage package of the current type declaring the method + * @return overridden method element + */ + private static Optional inherited(TypeInfo type, + TypedElementInfo method, + List arguments, + String currentPackage) { + + String methodName = method.elementName(); + // we look only for exact match (including types) + Optional found = type.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(ElementInfoPredicates.elementName(methodName)) + .filter(ElementInfoPredicates.hasParams(arguments)) + .findFirst(); + + if (found.isPresent()) { + TypedElementInfo superMethod = found.get(); + + // method has same signature, but is package local and is in a different package + boolean realOverride = superMethod.accessModifier() != AccessModifier.PACKAGE_PRIVATE + || currentPackage.equals(type.typeName().packageName()); + + if (realOverride) { + // this is a valid method that the type overrides + return Optional.of(superMethod); + } + } + + return Optional.empty(); + } + +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java b/codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java index e0a233f1d7d..79c0a7867eb 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java @@ -44,6 +44,7 @@ public abstract class TypeInfoFactoryBase { TypeName.create(Target.class), TypeName.create(Retention.class), TypeName.create(Repeatable.class)); + private static final Set ACCESS_MODIFIERS = Set.of("PUBLIC", "PRIVATE", "PROTECTED"); /** * There are no side effects of this constructor. @@ -144,10 +145,15 @@ protected static Set modifiers(CodegenContext Set result = new HashSet<>(); for (String stringModifier : stringModifiers) { + String upperCased = stringModifier.toUpperCase(Locale.ROOT); + if (ACCESS_MODIFIERS.contains(upperCased)) { + // ignore access modifiers, as they are handled elsewhere + continue; + } try { - result.add(io.helidon.common.types.Modifier.valueOf(stringModifier.toUpperCase(Locale.ROOT))); + result.add(io.helidon.common.types.Modifier.valueOf(upperCased)); } catch (Exception ignored) { - // we do not care about modifiers we do not understand - either access modifier, or something new + // we do not care about modifiers we do not understand ctx.logger().log(System.Logger.Level.TRACE, "Modifier " + stringModifier + " not understood by type info factory."); } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java index 674997b5f6e..0734834bd8e 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java @@ -44,6 +44,8 @@ default Set> supportedOptions() { * Annotations that are supported. * * @return set of annotation types + * @see io.helidon.codegen.RoundContext#annotatedTypes(io.helidon.common.types.TypeName) + * @see io.helidon.codegen.RoundContext#annotatedElements(io.helidon.common.types.TypeName) */ default Set supportedAnnotations() { return Set.of(); @@ -57,4 +59,15 @@ default Set supportedAnnotations() { default Set supportedAnnotationPackages() { return Set.of(); } + + /** + * Inherited annotations that are supported. + * If an annotation is annotated with this "meta" annotation, it is considered supported. + * + * @return set of meta annotation types + * @see io.helidon.codegen.RoundContext#annotatedAnnotations(io.helidon.common.types.TypeName) + */ + default Set supportedMetaAnnotations() { + return Set.of(); + } } diff --git a/codegen/codegen/src/test/java/io/helidon/codegen/CodegenValidatorTest.java b/codegen/codegen/src/test/java/io/helidon/codegen/CodegenValidatorTest.java new file mode 100644 index 00000000000..56b1fd018c0 --- /dev/null +++ b/codegen/codegen/src/test/java/io/helidon/codegen/CodegenValidatorTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen; + +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CodegenValidatorTest { + private static final TypeName ANNOTATION_TYPE = TypeName.create("io.helidon.test.MyAnnotation"); + private static final TypedElementInfo ELEMENT = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.PRIMITIVE_VOID) + .build(); + + @Test + public void testValidUri() { + String expected = "https://www.example.com/test"; + String actual = CodegenValidator.validateUri(TypeName.create(CodegenValidatorTest.class), + ELEMENT, + ANNOTATION_TYPE, + "uriProperty", + expected); + + assertThat(expected, is(actual)); + } + + @Test + public void testInvalidUri() { + String expected = "https:// www\\.example.com/test"; + var exception = assertThrows(CodegenException.class, + () -> CodegenValidator.validateUri(TypeName.create(CodegenValidatorTest.class), + ELEMENT, + ANNOTATION_TYPE, + "uriProperty", + expected)); + + String message = exception.getMessage(); + + assertThat(message, containsString("URI")); + assertThat(message, containsString("io.helidon.test.MyAnnotation.uriProperty()")); + assertThat(message, containsString(expected)); + } + + @Test + public void testValidDuration() { + String expected = "PT10.1S"; + String actual = CodegenValidator.validateDuration(TypeName.create(CodegenValidatorTest.class), + ELEMENT, + ANNOTATION_TYPE, + "durationProperty", + expected); + + assertThat(expected, is(actual)); + } + + @Test + public void testInvalidDuration() { + String expected = "10 minutes"; + var exception = assertThrows(CodegenException.class, + () -> CodegenValidator.validateDuration(TypeName.create(CodegenValidatorTest.class), + ELEMENT, + ANNOTATION_TYPE, + "durationProperty", + expected)); + + String message = exception.getMessage(); + + assertThat(message, containsString("Duration")); + assertThat(message, containsString("io.helidon.test.MyAnnotation.durationProperty()")); + assertThat(message, containsString(expected)); + } +} diff --git a/codegen/compiler/pom.xml b/codegen/compiler/pom.xml index ad41538b9d4..ab015c4d3f2 100644 --- a/codegen/compiler/pom.xml +++ b/codegen/compiler/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-compiler diff --git a/codegen/helidon-copyright/pom.xml b/codegen/helidon-copyright/pom.xml index 440145b8732..66ae03ed0af 100644 --- a/codegen/helidon-copyright/pom.xml +++ b/codegen/helidon-copyright/pom.xml @@ -23,8 +23,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT helidon-codegen-helidon-copyright diff --git a/codegen/pom.xml b/codegen/pom.xml index 40dc6a70b9a..b0671ab9845 100644 --- a/codegen/pom.xml +++ b/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.codegen @@ -60,4 +60,13 @@ + + + + tests + + tests + + + diff --git a/codegen/scan/pom.xml b/codegen/scan/pom.xml index ad049c8418e..ed85545c6af 100644 --- a/codegen/scan/pom.xml +++ b/codegen/scan/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-scan diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java index f7fd864644c..8d4c1738f24 100644 --- a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java @@ -18,18 +18,23 @@ import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import io.helidon.common.types.Annotation; +import io.helidon.common.types.EnumValue; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; import io.github.classgraph.AnnotationClassRef; import io.github.classgraph.AnnotationEnumValue; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.AnnotationParameterValue; import io.github.classgraph.AnnotationParameterValueList; +import io.github.classgraph.ClassInfo; /** * Factory for annotations. @@ -49,7 +54,44 @@ public static Annotation createAnnotation(ScanContext ctx, AnnotationInfo am) { TypeName typeName = ScanTypeFactory.create(am.getClassInfo()); - return Annotation.create(typeName, extractAnnotationValues(ctx, am)); + // ignore these annotations, unless one of them was explicitly requested + var set = new HashSet(); + set.add(TypeNames.INHERITED); + set.add(TypeNames.TARGET); + set.add(TypeNames.RETENTION); + set.add(TypeNames.DOCUMENTED); + set.remove(typeName); + + return createAnnotation(ctx, am, set) + .orElseThrow(); + } + + private static Optional createAnnotation(ScanContext ctx, AnnotationInfo am, HashSet processedTypes) { + ClassInfo classInfo = am.getClassInfo(); + if (classInfo == null) { + // cannot analyze this annotation + return Optional.empty(); + } + TypeName typeName = ScanTypeFactory.create(classInfo); + + if (processedTypes.contains(typeName)) { + return Optional.empty(); + } + var builder = Annotation.builder(); + + classInfo.getAnnotationInfo() + .stream() + .map(it -> { + var newProcessed = new HashSet<>(processedTypes); + newProcessed.add(typeName); + return createAnnotation(ctx, it, newProcessed); + }) + .flatMap(Optional::stream) + .forEach(builder::addMetaAnnotation); + + return Optional.of(builder.typeName(typeName) + .values(extractAnnotationValues(ctx, am)) + .build()); } /** @@ -85,15 +127,12 @@ private static Object toAnnotationValue(ScanContext ctx, Object scanAnnotationVa return result; } - if (scanAnnotationValue instanceof AnnotationEnumValue anEnum) { - return anEnum.getValueName(); - } else if (scanAnnotationValue instanceof AnnotationClassRef aClass) { - return TypeName.create(aClass.getName()); - } else if (scanAnnotationValue instanceof AnnotationInfo annotation) { - return createAnnotation(ctx, annotation); - } + return switch (scanAnnotationValue) { + case AnnotationEnumValue anEnum -> EnumValue.create(TypeName.create(anEnum.getClassName()), anEnum.getValueName()); + case AnnotationClassRef aClass -> TypeName.create(aClass.getName()); + case AnnotationInfo annotation -> createAnnotation(ctx, annotation); + default -> scanAnnotationValue; + }; - // supported type - return scanAnnotationValue; } } diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java index 6a9a0ae7053..ead7db43881 100644 --- a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java @@ -458,6 +458,21 @@ private static Set toModifiers(ClassMemberInfo memberInfo) { if (mi.isAbstract()) { result.add(Modifier.ABSTRACT); } + if (mi.isSynchronized()) { + result.add(Modifier.SYNCHRONIZED); + } + if (mi.isNative()) { + result.add(Modifier.NATIVE); + } + } + + if (memberInfo instanceof FieldInfo fi) { + if (java.lang.reflect.Modifier.isVolatile(fi.getModifiers())) { + result.add(Modifier.VOLATILE); + } + if (fi.isTransient()) { + result.add(Modifier.TRANSIENT); + } } return result; diff --git a/codegen/tests/pom.xml b/codegen/tests/pom.xml new file mode 100644 index 00000000000..aeed442c09b --- /dev/null +++ b/codegen/tests/pom.xml @@ -0,0 +1,50 @@ + + + + + io.helidon.codegen + helidon-codegen-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.codegen.tests + helidon-codegen-tests-project + + Helidon Codegen Tests Project + pom + + + true + true + true + true + true + true + true + + + + test-codegen + test-codegen-use + + diff --git a/codegen/tests/test-codegen-use/pom.xml b/codegen/tests/test-codegen-use/pom.xml new file mode 100644 index 00000000000..180c0b70a5b --- /dev/null +++ b/codegen/tests/test-codegen-use/pom.xml @@ -0,0 +1,98 @@ + + + + + io.helidon.codegen.tests + helidon-codegen-tests-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-codegen-tests-codegen-use + + Helidon Codegen Tests Codegen Use + + Usage of the code generator. + The main reason for this module is to make sure we can compile the generated class. + + + + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-class-model + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.codegen.tests + helidon-codegen-tests-codegen + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.codegen.tests + helidon-codegen-tests-codegen + ${helidon.version} + + + + + + diff --git a/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/CrazyAnnotation.java b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/CrazyAnnotation.java new file mode 100644 index 00000000000..5e9dc29b933 --- /dev/null +++ b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/CrazyAnnotation.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen.use; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CrazyAnnotation { + String stringValue(); + boolean booleanValue(); + long longValue(); + double doubleValue(); + int intValue(); + byte byteValue(); + char charValue(); + short shortValue(); + float floatValue(); + Class classValue(); + Class typeValue(); + ElementType enumValue(); + Target annotationValue(); + String[] lstring(); + boolean[] lboolean(); + long[] llong(); + double[] ldouble(); + int[] lint(); + byte[] lbyte(); + char[] lchar(); + short[] lshort(); + float[] lfloat(); + Class[] lclass(); + Class[] ltype(); + ElementType[] lenum(); + Target[] lannotation(); + String[] emptyList(); + String[] singletonList(); +} diff --git a/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/TriggerType.java b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/TriggerType.java new file mode 100644 index 00000000000..9f0be2a3640 --- /dev/null +++ b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/TriggerType.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen.use; + +import io.helidon.common.Weight; + +@Weight(48) +public final class TriggerType { + private transient volatile String field = "value"; + + public synchronized final String getField() { + return field; + } +} diff --git a/codegen/tests/test-codegen-use/src/main/java/module-info.java b/codegen/tests/test-codegen-use/src/main/java/module-info.java new file mode 100644 index 00000000000..8dc2fc7f9e9 --- /dev/null +++ b/codegen/tests/test-codegen-use/src/main/java/module-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module io.helidon.codegen.tests.codegen.use { + requires io.helidon.common; + requires io.helidon.common.types; +} \ No newline at end of file diff --git a/codegen/tests/test-codegen-use/src/test/java/io/helidon/codegen/test/codegen/use/CodegenValidationTest.java b/codegen/tests/test-codegen-use/src/test/java/io/helidon/codegen/test/codegen/use/CodegenValidationTest.java new file mode 100644 index 00000000000..0eb242d144d --- /dev/null +++ b/codegen/tests/test-codegen-use/src/test/java/io/helidon/codegen/test/codegen/use/CodegenValidationTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen.use; + +import java.lang.annotation.ElementType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsArrayContainingInOrder.arrayContaining; +import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize; +import static org.junit.jupiter.api.Assertions.fail; + +public class CodegenValidationTest { + private static final String CLASS_NAME = "io.helidon.codegen.test.codegen.use.TriggerType__Generated"; + private static Class clazz; + private static Object instance; + + @BeforeAll + public static void setUpClass() { + try { + clazz = Class.forName(CLASS_NAME); + } catch (ClassNotFoundException e) { + fail("Class " + CLASS_NAME + " should have been code generated by TestCodegenExtension"); + } + try { + instance = clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + fail("Class " + CLASS_NAME + " should have an accessible constructor", e); + } + } + + @Test + void testClassModifiers() throws ReflectiveOperationException { + String modifiers = (String) clazz.getMethod("classModifiers") + .invoke(instance); + + assertThat(modifiers, containsString("final")); + } + + @Test + void testMethodModifiers() throws ReflectiveOperationException { + String modifiers = (String) clazz.getMethod("methodModifiers") + .invoke(instance); + + assertThat(modifiers, containsString("final")); + assertThat(modifiers, containsString("synchronized")); + } + + @Test + void testFieldModifiers() throws ReflectiveOperationException { + String modifiers = (String) clazz.getMethod("fieldModifiers") + .invoke(instance); + + assertThat(modifiers, containsString("transient")); + assertThat(modifiers, containsString("volatile")); + } + + @Test + public void testGeneratedClass() { + CrazyAnnotation annotation = clazz.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + @Test + public void testGeneratedConstant() { + Field field; + try { + field = clazz.getDeclaredField("ANNOTATION"); + } catch (NoSuchFieldException e) { + fail("Class " + CLASS_NAME + " should contain a constant field \"ANNOTATION\", generated by TestCodegenExtension"); + return; + } + + Annotation annotation; + try { + annotation = (Annotation) field.get(null); + } catch (IllegalAccessException e) { + fail("Failed to get field ANNOTATION from the generated class " + CLASS_NAME, e); + return; + } + + assertThat(annotation, notNullValue()); + assertThat(annotation.typeName(), is(TypeName.create(CrazyAnnotation.class))); + assertThat(annotation.stringValue("stringValue"), optionalValue(is("value1"))); + assertThat(annotation.booleanValue("booleanValue"), optionalValue(is(true))); + assertThat(annotation.longValue("longValue"), optionalValue(is(49L))); + assertThat(annotation.doubleValue("doubleValue"), optionalValue(is(49D))); + assertThat(annotation.intValue("intValue"), optionalValue(is(49))); + assertThat(annotation.byteValue("byteValue"), optionalValue(is((byte) 49))); + assertThat(annotation.charValue("charValue"), optionalValue(is('x'))); + assertThat(annotation.shortValue("shortValue"), optionalValue(is((short) 49))); + assertThat(annotation.floatValue("floatValue"), optionalValue(is(49F))); + assertThat(annotation.typeValue("classValue"), optionalValue(is(TypeNames.STRING))); + assertThat(annotation.typeValue("typeValue"), optionalValue(is(TypeNames.STRING))); + assertThat(annotation.enumValue("enumValue", ElementType.class), + optionalValue(is(ElementType.FIELD))); + assertThat(annotation.annotationValue("annotationValue"), optionalPresent()); + + // lists + assertThat(annotation.stringValues("lstring"), + optionalValue(is(List.of("value1", "value2")))); + assertThat(annotation.booleanValues("lboolean") + , optionalValue(is(List.of(true, false)))); + assertThat(annotation.longValues("llong"), + optionalValue(is(List.of(49L, 50L)))); + assertThat(annotation.doubleValues("ldouble"), + optionalValue(is(List.of(49D, 50D)))); + assertThat(annotation.intValues("lint"), + optionalValue(is(List.of(49, 50)))); + assertThat(annotation.byteValues("lbyte"), + optionalValue(is(List.of((byte) 49, (byte) 50)))); + assertThat(annotation.charValues("lchar"), + optionalValue(is(List.of('x', 'y')))); + assertThat(annotation.shortValues("lshort"), + optionalValue(is(List.of((short) 49, (short) 50)))); + assertThat(annotation.floatValues("lfloat"), + optionalValue(is(List.of(49F, 50F)))); + assertThat(annotation.typeValues("lclass"), + optionalValue(is(List.of(TypeNames.STRING, TypeNames.BOXED_INT)))); + assertThat(annotation.typeValues("ltype"), + optionalValue(is(List.of(TypeNames.STRING, TypeNames.BOXED_INT)))); + assertThat(annotation.enumValues("lenum", ElementType.class), + optionalValue(is(List.of(ElementType.FIELD, ElementType.MODULE)))); + assertThat(annotation.annotationValues("lannotation"), optionalPresent()); + assertThat(annotation.stringValues("emptyList"), optionalValue(is(List.of()))); + assertThat(annotation.stringValues("singletonList"), optionalValue(is(List.of("value")))); + } + + @Test + public void testGeneratedField() { + Field field; + try { + field = clazz.getDeclaredField("field"); + } catch (NoSuchFieldException e) { + fail("Class " + CLASS_NAME + " should contain a field \"field\", generated by TestCodegenExtension"); + return; + } + + assertThat("Field type should be String", field.getType(), sameInstance(String.class)); + assertThat("Field should be private", Modifier.isPrivate(field.getModifiers())); + assertThat("Field should not be final", !Modifier.isFinal(field.getModifiers())); + assertThat("Field should not be static", !Modifier.isStatic(field.getModifiers())); + + CrazyAnnotation annotation = field.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + @Test + public void testGeneratedConstructor() { + Constructor ctr; + try { + ctr = clazz.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + fail("Class " + CLASS_NAME + " should contain a no-argument constructor, generated by TestCodegenExtension"); + return; + } + + assertThat("Constructor should be public", Modifier.isPublic(ctr.getModifiers())); + + CrazyAnnotation annotation = ctr.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + @Test + public void testGeneratedMethod() { + Method method; + try { + method = clazz.getDeclaredMethod("method"); + } catch (NoSuchMethodException e) { + fail("Class " + CLASS_NAME + " should contain a method \"method\", generated by TestCodegenExtension"); + return; + } + + assertThat("Method should be public", Modifier.isPublic(method.getModifiers())); + assertThat("Method should not be final", !Modifier.isFinal(method.getModifiers())); + assertThat("Method should not be static", !Modifier.isStatic(method.getModifiers())); + assertThat("Method return type should be void", method.getReturnType(), sameInstance(void.class)); + + CrazyAnnotation annotation = method.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + private void validateAnnotation(CrazyAnnotation annotation) { + // single values + assertThat(annotation.stringValue(), is("value1")); + assertThat(annotation.booleanValue(), is(true)); + assertThat(annotation.longValue(), is(49L)); + assertThat(annotation.doubleValue(), is(49D)); + assertThat(annotation.intValue(), is(49)); + assertThat(annotation.byteValue(), is((byte) 49)); + assertThat(annotation.charValue(), is('x')); + assertThat(annotation.shortValue(), is((short) 49)); + assertThat(annotation.floatValue(), is(49F)); + assertThat(annotation.classValue(), sameInstance(String.class)); + assertThat(annotation.typeValue(), sameInstance(String.class)); + assertThat(annotation.enumValue(), is(ElementType.FIELD)); + assertThat(annotation.annotationValue().value(), arrayContaining(ElementType.CONSTRUCTOR)); + + // arrays + assertThat(annotation.lstring(), arrayContaining("value1", "value2")); + + assertThat("Should be same boolean array, but is: " + Arrays.toString(annotation.lboolean()), + Arrays.equals(annotation.lboolean(), new boolean[] {true, false})); + assertThat("Should be same long array, but is: " + Arrays.toString(annotation.llong()), + Arrays.equals(annotation.llong(), new long[] {49L, 50L})); + assertThat("Should be same double array, but is: " + Arrays.toString(annotation.ldouble()), + Arrays.equals(annotation.ldouble(), new double[] {49D, 50D})); + assertThat("Should be same int array, but is: " + Arrays.toString(annotation.lint()), + Arrays.equals(annotation.lint(), new int[] {49, 50})); + assertThat("Should be same byte array, but is: " + Arrays.toString(annotation.lbyte()), + Arrays.equals(annotation.lbyte(), new byte[] {(byte) 49, (byte) 50})); + assertThat("Should be same char array, but is: " + Arrays.toString(annotation.lchar()), + Arrays.equals(annotation.lchar(), new char[] {'x', 'y'})); + assertThat("Should be same short array, but is: " + Arrays.toString(annotation.lshort()), + Arrays.equals(annotation.lshort(), new short[] {(short) 49, (short) 50})); + assertThat("Should be same float array, but is: " + Arrays.toString(annotation.lfloat()), + Arrays.equals(annotation.lfloat(), new float[] {49F, 50F})); + assertThat(annotation.lclass(), arrayContaining(String.class, Integer.class)); + assertThat(annotation.ltype(), arrayContaining(String.class, Integer.class)); + assertThat(annotation.lenum(), arrayContaining(ElementType.FIELD, ElementType.MODULE)); + assertThat(annotation.lannotation(), arrayWithSize(2)); + assertThat(annotation.emptyList(), arrayWithSize(0)); + assertThat(annotation.singletonList(), arrayContaining("value")); + } +} diff --git a/codegen/tests/test-codegen/pom.xml b/codegen/tests/test-codegen/pom.xml new file mode 100644 index 00000000000..5e2d75151ef --- /dev/null +++ b/codegen/tests/test-codegen/pom.xml @@ -0,0 +1,45 @@ + + + + + io.helidon.codegen.tests + helidon-codegen-tests-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-codegen-tests-codegen + + Helidon Codegen Tests Codegen + Testing code generator + + + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-class-model + + + diff --git a/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtension.java b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtension.java new file mode 100644 index 00000000000..fcd6a92788d --- /dev/null +++ b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtension.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.RoundContext; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +class TestCodegenExtension implements CodegenExtension { + private static final TypeName CRAZY = TypeName.create("io.helidon.codegen.test.codegen.use.CrazyAnnotation"); + private static final TypeName TARGET = TypeName.create(Target.class); + + private final CodegenContext ctx; + + TestCodegenExtension(CodegenContext ctx) { + this.ctx = ctx; + } + + @Override + public void process(RoundContext roundContext) { + Collection typeInfos = roundContext.annotatedTypes(TestCodegenExtensionProvider.WEIGHT); + for (TypeInfo typeInfo : typeInfos) { + process(typeInfo); + } + } + + private void process(TypeInfo typeInfo) { + TypeName typeName = typeInfo.typeName(); + TypeName generatedType = TypeName.builder() + .packageName(typeName.packageName()) + .className(typeName.className() + "__Generated") + .build(); + + var classModel = ClassModel.builder() + .type(generatedType) + .addAnnotation(annotation()) + .addField(f -> f + .isStatic(true) + .isFinal(true) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .name("ANNOTATION") + .type(Annotation.class) + .addContentCreate(annotation())) + .addField(f -> f + .name("field") + .type(String.class) + .addAnnotation(annotation())) + .addConstructor(ctr -> ctr.addAnnotation(annotation())) + .addMethod(method -> method + .name("method") + .addAnnotation(annotation())) + .addMethod(classMods -> classMods + .name("classModifiers") + .returnType(TypeNames.STRING) + .addContent("return \"") + .addContent(typeInfo.elementModifiers() + .stream() + .map(Modifier::modifierName) + .collect(Collectors.joining(","))) + .addContentLine("\";")); + + typeInfo.elementInfo() + .forEach(it -> { + if (it.kind() == ElementKind.METHOD) { + classModel.addMethod(methodMods -> methodMods + .name("methodModifiers") + .returnType(TypeNames.STRING) + .addContent("return \"") + .addContent(it.elementModifiers() + .stream() + .map(Modifier::modifierName) + .collect(Collectors.joining(","))) + .addContentLine("\";")); + } + if (it.kind() == ElementKind.FIELD) { + classModel.addMethod(fieldMods -> fieldMods + .name("fieldModifiers") + .returnType(TypeNames.STRING) + .addContent("return \"") + .addContent(it.elementModifiers() + .stream() + .map(Modifier::modifierName) + .collect(Collectors.joining(","))) + .addContentLine("\";")); + } + }); + + ctx.filer().writeSourceFile(classModel.build()); + } + + private Annotation annotation() { + return Annotation.builder() + .typeName(CRAZY) + .putValue("stringValue", "value1") + .putValue("booleanValue", true) + .putValue("longValue", 49L) + .putValue("doubleValue", 49.0D) + .putValue("intValue", 49) + .putValue("byteValue", (byte) 49) + .putValue("charValue", 'x') + .putValue("shortValue", (short) 49) + .putValue("floatValue", 49.0F) + .putValue("classValue", String.class) + .putValue("typeValue", TypeName.create(String.class)) + .putValue("enumValue", ElementType.FIELD) + .putValue("annotationValue", targetAnnotation(ElementType.CONSTRUCTOR)) + .putValue("lstring", List.of("value1", "value2")) + .putValue("lboolean", List.of(true, false)) + .putValue("llong", List.of(49L, 50L)) + .putValue("ldouble", List.of(49.0, 50.0)) + .putValue("lint", List.of(49, 50)) + .putValue("lbyte", List.of((byte) 49, (byte) 50)) + .putValue("lchar", List.of('x', 'y')) + .putValue("lshort", List.of((short) 49, (short) 50)) + .putValue("lfloat", List.of(49.0F, 50.0F)) + .putValue("lclass", List.of(String.class, Integer.class)) + .putValue("ltype", + List.of(TypeName.create(String.class), TypeName.create(Integer.class))) + .putValue("lenum", List.of(ElementType.FIELD, ElementType.MODULE)) + .putValue("lannotation", List.of(targetAnnotation(ElementType.CONSTRUCTOR), + targetAnnotation(ElementType.FIELD))) + .putValue("emptyList", List.of()) + .putValue("singletonList", List.of("value")) + .build(); + } + + private Annotation targetAnnotation(ElementType elementType) { + return Annotation.builder() + .typeName(TARGET) + .putValue("value", elementType) + .build(); + } +} diff --git a/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtensionProvider.java b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtensionProvider.java new file mode 100644 index 00000000000..b9966fb9745 --- /dev/null +++ b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtensionProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen; + +import java.util.Set; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.codegen.spi.CodegenExtensionProvider; +import io.helidon.common.Weight; +import io.helidon.common.types.TypeName; + +public class TestCodegenExtensionProvider implements CodegenExtensionProvider { + static final TypeName WEIGHT = TypeName.create(Weight.class); + + @Override + public CodegenExtension create(CodegenContext ctx, TypeName generatorType) { + return new TestCodegenExtension(ctx); + } + + @Override + public Set supportedAnnotations() { + return Set.of(WEIGHT); + } +} diff --git a/codegen/tests/test-codegen/src/main/java/module-info.java b/codegen/tests/test-codegen/src/main/java/module-info.java new file mode 100644 index 00000000000..092592da857 --- /dev/null +++ b/codegen/tests/test-codegen/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.codegen.test.codegen.TestCodegenExtensionProvider; + +module io.helidon.codegen.tests.codegen { + requires io.helidon.codegen; + requires io.helidon.codegen.classmodel; + + provides io.helidon.codegen.spi.CodegenExtensionProvider + with TestCodegenExtensionProvider; +} \ No newline at end of file diff --git a/common/buffers/pom.xml b/common/buffers/pom.xml index 8b6602c285e..cc5c6510bec 100644 --- a/common/buffers/pom.xml +++ b/common/buffers/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-buffers Helidon Common Buffers diff --git a/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java b/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java index f6ec7a596d1..b7153718d9a 100644 --- a/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java +++ b/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,16 @@ package io.helidon.common.buffers; +import java.nio.ByteOrder; + +/* + * SWAR related methods in this class are heavily inspired by: + * PR https://github.com/netty/netty/pull/10737 + * + * From Netty + * Distributed under Apache License, Version 2.0 + */ + /** * Bytes commonly used in HTTP. */ @@ -69,6 +79,151 @@ public final class Bytes { */ public static final byte TAB_BYTE = (byte) '\t'; + private static final boolean BYTE_ORDER_LE = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN; + private Bytes() { } + + /** + * This is using a SWAR (SIMD Within A Register) batch read technique to minimize bound-checks and improve memory + * usage while searching for {@code value}. + *

    + * This method does NOT do a bound check on the buffer length and the fromIndex and toIndex, neither does it check + * if the {@code toIndex} is bigger than the {@code fromIndex}. + * + * @param buffer the byte buffer to search + * @param fromIndex first index in the array + * @param toIndex last index in the array + * @param value to search for + * @return first index of the desired byte {@code value}, or {@code -1} if not found + */ + public static int firstIndexOf(byte[] buffer, int fromIndex, int toIndex, byte value) { + if (fromIndex == toIndex || buffer.length == 0) { + // fast path for empty buffers, or empty range + return -1; + } + int length = toIndex - fromIndex; + int offset = fromIndex; + int byteCount = length & 7; + if (byteCount > 0) { + int index = unrolledFirstIndexOf(buffer, fromIndex, byteCount, value); + if (index != -1) { + return index; + } + offset += byteCount; + if (offset == toIndex) { + return -1; + } + } + int longCount = length >>> 3; + long pattern = compilePattern(value); + for (int i = 0; i < longCount; i++) { + // use the faster available getLong + long word = toWord(buffer, offset); + int index = firstInstance(word, pattern); + if (index < Long.BYTES) { + return offset + index; + } + offset += Long.BYTES; + } + return -1; + } + + /** + * Converts the first 8 bytes from {@code offset} to a long, using appropriate byte order for this machine. + *

      + *
    • This method DOES NOT do a bound check
    • + *
    • This method DOES NOT validate there are 8 bytes available
    • + *
    + * + * @param buffer bytes to convert + * @param offset offset within the byte array + * @return long word from the first 8 bytes from offset + */ + public static long toWord(byte[] buffer, int offset) { + return BYTE_ORDER_LE ? toWordLe(buffer, offset) : toWordBe(buffer, offset); + } + + // create a pattern for a byte, so we can search for it in a whole word + private static long compilePattern(byte byteToFind) { + return (byteToFind & 0xFFL) * 0x101010101010101L; + } + + // first instance of a pattern within a word (see above compilePattern) + private static int firstInstance(long word, long pattern) { + long input = word ^ pattern; + long tmp = (input & 0x7F7F7F7F7F7F7F7FL) + 0x7F7F7F7F7F7F7F7FL; + tmp = ~(tmp | input | 0x7F7F7F7F7F7F7F7FL); + int binaryPosition = Long.numberOfTrailingZeros(tmp); + return binaryPosition >>> 3; + } + + // to a little endian word + private static long toWordLe(byte[] buffer, int index) { + return (long) buffer[index] & 0xff + | ((long) buffer[index + 1] & 0xff) << 8 + | ((long) buffer[index + 2] & 0xff) << 16 + | ((long) buffer[index + 3] & 0xff) << 24 + | ((long) buffer[index + 4] & 0xff) << 32 + | ((long) buffer[index + 5] & 0xff) << 40 + | ((long) buffer[index + 6] & 0xff) << 48 + | ((long) buffer[index + 7] & 0xff) << 56; + } + + private static long toWordBe(byte[] buffer, int index) { + return ((long) buffer[index] & 0xff) << 56 + | ((long) buffer[index + 1] & 0xff) << 48 + | ((long) buffer[index + 2] & 0xff) << 40 + | ((long) buffer[index + 3] & 0xff) << 32 + | ((long) buffer[index + 4] & 0xff) << 24 + | ((long) buffer[index + 5] & 0xff) << 16 + | ((long) buffer[index + 6] & 0xff) << 8 + | (long) buffer[index + 7] & 0xff; + } + + // this method is copied from Netty, and validated by them that it is the optimal + // way to figure out the index, see https://github.com/netty/netty/issues/10731 + private static int unrolledFirstIndexOf(byte[] buffer, int fromIndex, int byteCount, byte value) { + assert byteCount > 0 && byteCount < 8; + if (buffer[fromIndex] == value) { + return fromIndex; + } + if (byteCount == 1) { + return -1; + } + if (buffer[fromIndex + 1] == value) { + return fromIndex + 1; + } + if (byteCount == 2) { + return -1; + } + if (buffer[fromIndex + 2] == value) { + return fromIndex + 2; + } + if (byteCount == 3) { + return -1; + } + if (buffer[fromIndex + 3] == value) { + return fromIndex + 3; + } + if (byteCount == 4) { + return -1; + } + if (buffer[fromIndex + 4] == value) { + return fromIndex + 4; + } + if (byteCount == 5) { + return -1; + } + if (buffer[fromIndex + 5] == value) { + return fromIndex + 5; + } + if (byteCount == 6) { + return -1; + } + if (buffer[fromIndex + 6] == value) { + return fromIndex + 6; + } + return -1; + } } diff --git a/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java b/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java index 0777387ceb8..5342a13e681 100644 --- a/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java +++ b/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java @@ -289,6 +289,36 @@ public String readAsciiString(int len) { } } + /** + * Read byte array. + * + * @param len number of bytes of the string + * @return string value + */ + public byte[] readBytes(int len) { + ensureAvailable(); // we have at least 1 byte + byte[] b = new byte[len]; + + if (len <= head.available()) { // fast case + System.arraycopy(head.bytes, head.position, b, 0, len); + head.position += len; + return b; + } else { + int remaining = len; + for (Node n = head; remaining > 0; n = n.next) { + ensureAvailable(); + int toAdd = Math.min(remaining, n.available()); + System.arraycopy(n.bytes, n.position, b, len - remaining, toAdd); + remaining -= toAdd; + n.position += toAdd; + if (remaining > 0 && n.next == null) { + pullData(); + } + } + return b; + } + } + /** * Read an ascii string until new line. * @@ -365,33 +395,62 @@ public String debugDataHex() { */ public int findNewLine(int max) throws IncorrectNewLineException { ensureAvailable(); - int idx = 0; Node n = head; + int idx = 0; + int fromIndexNode = n.position; + while (true) { byte[] barr = n.bytes; - for (int i = n.position; i < barr.length && idx < max; i++, idx++) { - if (barr[i] == Bytes.LF_BYTE && !ignoreLoneEol) { - throw new IncorrectNewLineException("Found LF (" + idx + ") without preceding CR. :\n" + this.debugDataHex()); - } else if (barr[i] == Bytes.CR_BYTE) { - byte nextByte; - if (i + 1 < barr.length) { - nextByte = barr[i + 1]; - } else { - nextByte = n.next().peek(); + int maxLength = Math.min(max - idx, barr.length - fromIndexNode); + int crIndexNode = Bytes.firstIndexOf(barr, fromIndexNode, fromIndexNode + maxLength, Bytes.CR_BYTE); + + if (crIndexNode == -1) { + int lfIndexNode = Bytes.firstIndexOf(barr, fromIndexNode, fromIndexNode + maxLength, Bytes.LF_BYTE); + if (lfIndexNode != -1) { + if (!ignoreLoneEol) { + throw new IncorrectNewLineException("Found LF (" + (idx + lfIndexNode - n.position) + + ") without preceding CR. :\n" + this.debugDataHex()); + } + } + } else { + // found, next byte should be LF + if (crIndexNode == barr.length - 1) { + // found CR as the last byte of the current node, peek next node + byte nextByte = n.next().peek(); + if (nextByte == Bytes.LF_BYTE) { + return idx + crIndexNode - fromIndexNode; } + if (!ignoreLoneEol) { + throw new IncorrectNewLineException("Found CR (" + (idx + crIndexNode - n.position) + + ") without following LF. :\n" + this.debugDataHex()); + } + } else { + // found CR within the current array + byte nextByte = barr[crIndexNode + 1]; if (nextByte == Bytes.LF_BYTE) { - return idx; + return idx + crIndexNode - fromIndexNode; } if (!ignoreLoneEol) { throw new IncorrectNewLineException("Found CR (" + idx + ") without following LF. :\n" + this.debugDataHex()); } + idx += (crIndexNode - fromIndexNode + 1); + fromIndexNode = crIndexNode + 1; + if (idx >= max) { + return max; + } + continue; } } - if (idx == max) { + + // not found, continue with next buffer + idx += maxLength; + if (idx >= max) { + // not found and reached the limit return max; } n = n.next(); + fromIndexNode = n.position; } } diff --git a/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java b/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java index 4843e6bac84..3e5ea5e928b 100644 --- a/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java +++ b/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,12 @@ * String that materializes only when requested. */ public class LazyString { + private static final boolean[] IS_OWS = new boolean[256]; + static { + IS_OWS[Bytes.SPACE_BYTE] = true; + IS_OWS[Bytes.TAB_BYTE] = true; + } + private final byte[] buffer; private final int offset; private final int length; @@ -69,9 +75,9 @@ public String stripOws() { int newOffset = offset; int newLength = length; for (int i = offset; i < offset + length; i++) { - if (isOws(buffer[i])) { + if (IS_OWS[buffer[i] & 0xff]) { newOffset = i + 1; - newLength = length - (newOffset - offset); + newLength--; } else { // no more white space, go from the end now break; @@ -79,7 +85,7 @@ public String stripOws() { } // now we need to go from the end of the string for (int i = offset + length - 1; i > newOffset; i--) { - if (isOws(buffer[i])) { + if (IS_OWS[buffer[i] & 0xff]) { newLength--; } else { break; @@ -99,11 +105,4 @@ public String toString() { } return stringValue; } - - private boolean isOws(byte aByte) { - return switch (aByte) { - case Bytes.SPACE_BYTE, Bytes.TAB_BYTE -> true; - default -> false; - }; - } } diff --git a/common/buffers/src/test/java/io/helidon/common/buffers/AsciiTest.java b/common/buffers/src/test/java/io/helidon/common/buffers/AsciiTest.java index 4ae366d9fa2..307d74efe69 100644 --- a/common/buffers/src/test/java/io/helidon/common/buffers/AsciiTest.java +++ b/common/buffers/src/test/java/io/helidon/common/buffers/AsciiTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,24 +19,21 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; class AsciiTest { @Test void testIsLowerCaseOne() { - assertFalse(Ascii.isLowerCase('{')); + assertThat(Ascii.isLowerCase('{'), is(false)); } @Test void testIsLowerCaseReturningTrue() { - assertTrue(Ascii.isLowerCase('o')); + assertThat(Ascii.isLowerCase('o'), is(true)); } @Test void testIsLowerCaseTwo() { - assertFalse(Ascii.isLowerCase('\"')); + assertThat(Ascii.isLowerCase('\"'), is(false)); } @Test @@ -52,32 +49,32 @@ void testToLowerCaseChar() { void testToLowerCaseTakingCharSequenceOne() { StringBuilder stringBuilder = new StringBuilder("uhho^s} b'jdwtym"); - assertEquals("uhho^s} b'jdwtym", Ascii.toLowerCase(stringBuilder)); + assertThat(Ascii.toLowerCase(stringBuilder), is("uhho^s} b'jdwtym")); } @Test void testToLowerCaseTakingCharSequenceTwo() { - assertEquals("uhho^s} b'jdwtym", Ascii.toLowerCase((CharSequence) "uHHO^S} b'jDwTYM")); + assertThat(Ascii.toLowerCase((CharSequence) "uHHO^S} b'jDwTYM"), is("uhho^s} b'jdwtym")); } @Test void testToLowerCaseTakingString() { - assertEquals("", Ascii.toLowerCase("")); + assertThat(Ascii.toLowerCase(""), is("")); } @Test void testIsUpperCaseOne() { - assertFalse(Ascii.isUpperCase('{')); + assertThat(Ascii.isUpperCase('{'), is(false)); } @Test void testIsUpperCaseReturningTrue() { - assertTrue(Ascii.isUpperCase('O')); + assertThat(Ascii.isUpperCase('O'), is(true)); } @Test void testIsUpperCaseTwo() { - assertFalse(Ascii.isUpperCase('\"')); + assertThat(Ascii.isUpperCase('\"'), is(false)); } @Test @@ -93,16 +90,16 @@ void testToUpperCaseChar() { void testToUpperCaseTakingCharSequenceOne() { StringBuilder stringBuilder = new StringBuilder("UhHO^S} B'JDWTYM"); - assertEquals("UHHO^S} B'JDWTYM", Ascii.toUpperCase(stringBuilder)); + assertThat(Ascii.toUpperCase(stringBuilder), is("UHHO^S} B'JDWTYM")); } @Test void testToUpperCaseTakingCharSequenceTwo() { - assertEquals("UHHO^S} B'JDWTYM", Ascii.toUpperCase((CharSequence) "uHHO^S} b'jDwTYM")); + assertThat(Ascii.toUpperCase((CharSequence) "uHHO^S} b'jDwTYM"), is("UHHO^S} B'JDWTYM")); } @Test void testToUpperCaseTakingString() { - assertEquals("UHHO^S} B'JDWTYM", Ascii.toUpperCase("uHHO^S} b'jDwTYM")); + assertThat(Ascii.toUpperCase("uHHO^S} b'jDwTYM"), is("UHHO^S} B'JDWTYM")); } } \ No newline at end of file diff --git a/common/buffers/src/test/java/io/helidon/common/buffers/DataReaderTest.java b/common/buffers/src/test/java/io/helidon/common/buffers/DataReaderTest.java new file mode 100644 index 00000000000..e37312e1d03 --- /dev/null +++ b/common/buffers/src/test/java/io/helidon/common/buffers/DataReaderTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.buffers; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class DataReaderTest { + + @Test + void testFindNewLineWithLoneCR() { + // reading N bytes at a time until a new line is found + // with data containing a lone CR + + byte[] data = "00\r0\r\n".getBytes(StandardCharsets.US_ASCII); + AtomicReference ref = new AtomicReference<>(data); + DataReader dataReader = new DataReader(() -> ref.getAndSet(null), true); + + int n = 2; + assertThat(dataReader.findNewLine(n), is(n)); + dataReader.skip(n); + assertThat(dataReader.findNewLine(n), is(n)); + dataReader.skip(n); + assertThat(dataReader.findNewLine(n), is(0)); + } + + @Test + void testFindNewLineWithMultipleLoneCR() { + // if the stream index is accumulated with the node index for each lone CR + // it may exceed max and the new line is ignored + + byte[] data = "00\r\r\r\n".getBytes(StandardCharsets.US_ASCII); + AtomicReference ref = new AtomicReference<>(data); + DataReader dataReader = new DataReader(() -> ref.getAndSet(null), true); + + int n = 5; + assertThat(dataReader.findNewLine(n), is(4)); + } + + @Test + void testFindNewLineWithMultipleLoneWithinMax() { + // if the stream index is not updated for each lone CR + // the computed search range is too big and a value greater than max is returned + + byte[] data = "00\r00\r\n00".getBytes(StandardCharsets.US_ASCII); + AtomicReference ref = new AtomicReference<>(data); + DataReader dataReader = new DataReader(() -> ref.getAndSet(null), true); + + int n = 4; + assertThat(dataReader.findNewLine(n), is(n)); + dataReader.skip(n); + assertThat(dataReader.findNewLine(n), is(1)); + } +} diff --git a/common/common/pom.xml b/common/common/pom.xml index 041db05b592..791e8756e85 100644 --- a/common/common/pom.xml +++ b/common/common/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common Helidon Common diff --git a/common/common/src/main/java/io/helidon/common/Size.java b/common/common/src/main/java/io/helidon/common/Size.java new file mode 100644 index 00000000000..b888d8b4e09 --- /dev/null +++ b/common/common/src/main/java/io/helidon/common/Size.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A definition of size in bytes. + */ +public interface Size { + /** + * Empty size - zero bytes. + */ + Size ZERO = Size.create(0); + + /** + * Create a new size with explicit number of bytes. + * + * @param size number of bytes + * @return a new size instance + */ + static Size create(long size) { + return new SizeImpl(BigInteger.valueOf(size)); + } + + /** + * Create a new size from amount and unit. + * + * @param amount amount in the provided unit + * @param unit unit + * @return size representing the amount + */ + static Size create(long amount, Unit unit) { + Objects.requireNonNull(unit, "Unit must not be null"); + + return new SizeImpl(BigInteger.valueOf(amount).multiply(unit.bytesInteger())); + } + + /** + * Create a new size from amount and unit. + * + * @param amount amount that can be decimal + * @param unit unit + * @return size representing the amount + * @throws IllegalArgumentException in case the amount cannot be converted to whole bytes (i.e. it has + * a fraction of byte) + */ + static Size create(BigDecimal amount, Unit unit) { + Objects.requireNonNull(amount, "Amount must not be null"); + Objects.requireNonNull(unit, "Unit must not be null"); + + BigDecimal result = amount.multiply(new BigDecimal(unit.bytesInteger())); + return new SizeImpl(result.toBigIntegerExact()); + } + + /** + * Crete a new size from the size string. + * The string may contain a unit. If a unit is not present, the size string is considered to be number of bytes. + *

    + * We understand units from kilo (meaning 1000 or 1024, see table below), to exa bytes. + * Each higher unit is either 1000 times or 1024 times bigger than the one below, depending on the approach used. + *

    + * Measuring approaches and their string representations: + *

      + *
    • KB, KiB - kibi, kilobinary, stands for 1024 bytes
    • + *
    • kB, kb - kilobyte, stands for 1000 bytes
    • + *
    • MB, MiB - mebi, megabinary, stands for 1024*1024 bytes
    • + *
    • mB, mb - megabyte, stands for 1000*1000 bytes
    • + *
    • From here the same concept is applied with Giga, Tera, Peta, and Exa bytes
    • + *
    + * + * @param sizeString the string definition, such as {@code 76 MB}, or {@code 976 mB}, can also be a decimal number + * - we use {@link java.math.BigDecimal} to parse the numeric section of the size; if there is a unit + * defined, it must be separated by a single space from the numeric section + * @return parsed size that can provide exact number of bytes + */ + static Size parse(String sizeString) { + Objects.requireNonNull(sizeString, "Size string is null"); + + String parsed = sizeString.trim(); + if (parsed.isEmpty()) { + throw new IllegalArgumentException("Size string is empty."); + } + int lastSpace = parsed.lastIndexOf(' '); + if (lastSpace == -1) { + // no unit + return create(new BigDecimal(parsed), Unit.BYTE); + } + String size = parsed.substring(0, lastSpace); + Unit unit = Unit.parse(parsed.substring(lastSpace + 1)); + BigDecimal amount = new BigDecimal(size); + return create(amount, unit); + } + + /** + * Amount of units in this size. + * + * @param unit to get the size of + * @return size in the provided unit as a big decimal + * @throws ArithmeticException in case this size cannot be converted to the specified unit without losing + * information + * @see #toBytes() + */ + BigDecimal to(Unit unit); + + /** + * Number of bytes this size represents. + * + * @return number of bytes + * @throws ArithmeticException in case the amount is higher than {@link Long#MAX_VALUE}, or would contain + * fractions of byte + */ + long toBytes(); + + /** + * Get the highest possible unit of the size with integer amount. + * + * @param unitKind kind of unit to print (kB, kb, KB, or KiB) + * @return amount integer with a unit, such as {@code 270 kB}, if the amount is {@code 2000 kB}, this method would return + * {@code 2 mB} instead for {@link io.helidon.common.Size.UnitKind#DECIMAL_UPPER_CASE} + */ + String toString(UnitKind unitKind); + + /** + * Get the amount in the provided unit as a decimal number if needed. If the amount cannot be correctly + * expressed in the provided unit, an exception is thrown. + * + * @param unit unit to use, such as {@link io.helidon.common.Size.Unit#MIB} + * @param unitKind kind of unit for the output, must match the provided unit, + * such as {@link io.helidon.common.Size.UnitKind#BINARY_BI} to print {@code MiB} + * @return amount decimal with a unit, such as {@code 270.426 MiB} + * @throws java.lang.IllegalArgumentException in case the unitKind does not match the unit + */ + String toString(Unit unit, UnitKind unitKind); + + /** + * Kind of units, used for printing out the correct unit. + */ + enum UnitKind { + /** + * The first letter (if two lettered) is lower case, the second is upper case, such ase + * {@code B, kB, mB}. These represent powers of 1000. + */ + DECIMAL_UPPER_CASE(false), + /** + * All letters are lower case, such as + * {@code b, kb, mb}. These represent powers of 1000. + */ + DECIMAL_LOWER_CASE(false), + /** + * The multiplier always contains {@code i}, the second is upper case B, such ase + * {@code B, KiB, MiB}. These represent powers of 1024. + */ + BINARY_BI(true), + /** + * All letters are upper case, such as + * {@code B, KB, MB}. These represent powers of 1024. + */ + BINARY_UPPER_CASE(true); + private final boolean isBinary; + + UnitKind(boolean isBinary) { + this.isBinary = isBinary; + } + + boolean isBinary() { + return isBinary; + } + } + + /** + * Units that can be used. + */ + enum Unit { + /** + * Bytes. + */ + BYTE(1024, 0, "b", "B"), + /** + * Kilobytes (represented as {@code kB}), where {@code kilo} is used in its original meaning as a thousand, + * i.e. 1 kB is 1000 bytes. + */ + KB(1000, 1, "kB", "kb"), + /** + * Kibi-bytes (represented as either {@code KB} or {@code KiB}), where we use binary approach, i.e. + * 1 KB or KiB is 1024 bytes. + */ + KIB(1024, 1, "KB", "KiB"), + /** + * Megabytes (represented as {@code mB}), where {@code mega} is used in its original meaning as a million, + * i.e. 1 mB is 1000^2 bytes (1000 to the power of 2), or 1000 kB. + */ + MB(1000, 2, "mB", "mb"), + /** + * Mebi-bytes (represented as either {@code MB} or {@code MiB}), where we use binary approach, i.e. + * 1 MB or MiB is 1024^2 bytes (1024 to the power 2), or 1024 KiB. + */ + MIB(1024, 2, "MB", "MiB"), + /** + * Gigabytes (represented as {@code gB}): + * i.e. 1 gB is 1000^3 bytes (1000 to the power of 3), or 1000 mB. + */ + GB(1000, 3, "gB", "gb"), + /** + * Gibi-bytes (represented as either {@code GB} or {@code GiB}), where we use binary approach, i.e. + * 1 GB or GiB is 1024^3 bytes (1024 to the power 3), or 1024 MiB. + */ + GIB(1024, 3, "GB", "GiB"), + /** + * Terabytes (represented as {@code tB}): + * i.e. 1 gB is 1000^4 bytes (1000 to the power of 4), or 1000 gB. + */ + TB(1000, 4, "tB", "tb"), + /** + * Tebi-bytes (represented as either {@code TB} or {@code TiB}), where we use binary approach, i.e. + * 1 TB or TiB is 1024^4 bytes (1024 to the power 4), or 1024 GiB. + */ + TIB(1024, 4, "TB", "TiB"), + /** + * Petabytes (represented as {@code pB}): + * i.e. 1 pB is 1000^5 bytes (1000 to the power of 5), or 1000 tB. + */ + PB(1000, 5, "pB", "pb"), + /** + * Pebi-bytes (represented as either {@code PB} or {@code PiB}), where we use binary approach, i.e. + * 1 PB or PiB is 1024^5 bytes (1024 to the power 5), or 1024 TiB. + */ + PIB(1024, 5, "PB", "PiB"), + /** + * Exabytes (represented as {@code eB}): + * i.e. 1 eB is 1000^6 bytes (1000 to the power of 6), or 1000 pB. + */ + EB(1000, 6, "eB", "eb"), + /** + * Exbi-bytes (represented as either {@code EB} or {@code EiB}), where we use binary approach, i.e. + * 1 EB or EiB is 1024^6 bytes (1024 to the power 6), or 1024 PiB. + */ + EIB(1024, 6, "EB", "EiB"); + + private static final Map UNIT_MAP; + + static { + Map units = new HashMap<>(); + for (Unit unit : Unit.values()) { + for (String validUnitString : unit.units) { + units.put(validUnitString, unit); + } + } + UNIT_MAP = Map.copyOf(units); + } + + private final long bytes; + private final int power; + private final BigInteger bytesInteger; + private final Set units; + private final boolean binary; + private final String firstUnit; + private final String secondUnit; + + /** + * Unit. + * + * @param base base of the calculation (1000 or 1024) + * @param power to the power of + * @param firstUnit first unit (either upper case decimal [mB], or all upper case [MB]) + * @param secondUnit second unit (either lower case decimal [mb], or binary unit name [MiB]) + */ + Unit(int base, int power, String firstUnit, String secondUnit) { + this.firstUnit = firstUnit; + this.secondUnit = secondUnit; + this.units = Set.of(firstUnit, secondUnit); + this.bytes = (long) Math.pow(base, power); + this.bytesInteger = BigInteger.valueOf(bytes); + this.power = power; + this.binary = base == 1024; + } + + /** + * Parse the size string to appropriate unit. + * + * @param unitString defines the unit, such as {@code KB}, {@code MiB}, {@code pB} etc.; empty string parses to + * {@link #BYTE} + * @return a parsed unit + * @throws IllegalArgumentException if the unit cannot be parsed + */ + public static Unit parse(String unitString) { + if (unitString.isEmpty()) { + return BYTE; + } + Unit unit = UNIT_MAP.get(unitString); + if (unit == null) { + throw new IllegalArgumentException("Unknown unit: " + unitString); + } + return unit; + } + + /** + * Number of bytes this unit represents. + * + * @return number of bytes of this unit + */ + public long bytes() { + return bytes; + } + + /** + * Number of bytes in this unit (exact integer). + * + * @return number of bytes this unit contains + */ + public BigInteger bytesInteger() { + return bytesInteger; + } + + String unitString(UnitKind unitKind) { + if (power == 0) { + if (unitKind == UnitKind.DECIMAL_LOWER_CASE) { + return "b"; + } + return "B"; + } + + return switch (unitKind) { + case DECIMAL_UPPER_CASE, BINARY_UPPER_CASE -> firstUnit; + case DECIMAL_LOWER_CASE, BINARY_BI -> secondUnit; + }; + } + + boolean isBinary() { + return binary; + } + } +} diff --git a/common/common/src/main/java/io/helidon/common/SizeImpl.java b/common/common/src/main/java/io/helidon/common/SizeImpl.java new file mode 100644 index 00000000000..e89b2bcbc0e --- /dev/null +++ b/common/common/src/main/java/io/helidon/common/SizeImpl.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.util.Objects; + +class SizeImpl implements Size { + private final BigInteger bytes; + + SizeImpl(BigInteger bytes) { + this.bytes = bytes; + } + + @Override + public BigDecimal to(Unit unit) { + Objects.requireNonNull(unit, "Unit must not be null"); + + BigDecimal bigDecimal = new BigDecimal(unit.bytesInteger()); + BigDecimal result = new BigDecimal(bytes).divide(bigDecimal, + bigDecimal.precision() + 1, + RoundingMode.UNNECESSARY); + return result.stripTrailingZeros(); + } + + @Override + public long toBytes() { + try { + return bytes.longValueExact(); + } catch (ArithmeticException e) { + // we cannot use a cause with constructor, creating a more descriptive message + throw new ArithmeticException("Size " + this + " cannot be converted to number of bytes, out of long range."); + } + } + + @Override + public String toString(UnitKind unitKind) { + Objects.requireNonNull(unitKind, "Unit kind must not be null"); + + if (bytes.equals(BigInteger.ZERO)) { + return "0 " + Unit.BYTE.unitString(unitKind); + } + + // try each amount from the highest that returns zero decimal places + Unit[] values = Unit.values(); + for (int i = values.length - 1; i >= 0; i--) { + Unit value = values[i]; + if (value.isBinary() != unitKind.isBinary()) { + continue; + } + BigDecimal bigDecimal = to(value); + try { + // try to convert without any decimal spaces + BigInteger bi = bigDecimal.toBigIntegerExact(); + return bi + " " + value.unitString(unitKind); + } catch (Exception ignored) { + // ignored, we cannot convert to this unit, because it cannot be correctly divided + } + } + + return bytes + " " + Unit.BYTE.unitString(unitKind); + } + + @Override + public String toString(Unit unit, UnitKind unitKind) { + Objects.requireNonNull(unit, "Unit must not be null"); + Objects.requireNonNull(unitKind, "Unit kind must not be null"); + + if (unit.isBinary() != unitKind.isBinary()) { + throw new IllegalArgumentException("Unit " + unit + " does not match kind " + unitKind); + } + String unitString = unit.unitString(unitKind); + BigDecimal amount = to(unit); + + return amount.toPlainString() + " " + unitString; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Size size)) { + return false; + } + return Objects.equals(size.to(Unit.BYTE), this.to(Unit.BYTE)); + } + + @Override + public int hashCode() { + return Objects.hash(to(Unit.BYTE)); + } + + @Override + public String toString() { + return toString(UnitKind.DECIMAL_UPPER_CASE); + } +} diff --git a/common/common/src/test/java/io/helidon/common/SizeTest.java b/common/common/src/test/java/io/helidon/common/SizeTest.java new file mode 100644 index 00000000000..73be10f677b --- /dev/null +++ b/common/common/src/test/java/io/helidon/common/SizeTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SizeTest { + @Test + void testBytesEmpty() { + Size first = Size.create(0); + Size second = Size.ZERO; + + assertThat(first, is(second)); + assertThat(first.hashCode(), is(second.hashCode())); + + assertThat(first.toBytes(), is(0L)); + assertThat(second.toBytes(), is(0L)); + + for (Size.Unit unit : Size.Unit.values()) { + assertThat(first.to(unit), is(BigDecimal.ZERO)); + assertThat(second.to(unit), is(BigDecimal.ZERO)); + } + + assertThat(first.toString(), is("0 B")); + assertThat(first.toString(Size.UnitKind.DECIMAL_LOWER_CASE), is("0 b")); + assertThat(first.toString(Size.Unit.EIB, Size.UnitKind.BINARY_BI), is("0 EiB")); + } + + @Test + void testTooBig() { + Size size = Size.create(Long.MAX_VALUE, Size.Unit.KB); + assertThrows(ArithmeticException.class, size::toBytes); + } + + @Test + void testToStringWrongUnitKind() { + Size size = Size.create(1024, Size.Unit.KIB); + assertThrows(IllegalArgumentException.class, () -> size.toString(Size.Unit.EB, Size.UnitKind.BINARY_BI)); + assertThrows(IllegalArgumentException.class, () -> size.toString(Size.Unit.EB, Size.UnitKind.BINARY_UPPER_CASE)); + assertThrows(IllegalArgumentException.class, () -> size.toString(Size.Unit.EIB, Size.UnitKind.DECIMAL_UPPER_CASE)); + assertThrows(IllegalArgumentException.class, () -> size.toString(Size.Unit.EIB, Size.UnitKind.DECIMAL_LOWER_CASE)); + } + + @Test + void testConversionsBinary() { + Size size = Size.create(1, Size.Unit.EIB); + + assertThat(size.toBytes(), is(1152921504606846976L)); + + assertThat(size.to(Size.Unit.BYTE), is(new BigDecimal(Size.Unit.EIB.bytesInteger()))); + assertThat(size.to(Size.Unit.KIB), is(BigDecimal.valueOf(1125899906842624L))); + assertThat(size.to(Size.Unit.MIB), is(BigDecimal.valueOf(1099511627776L))); + assertThat(size.to(Size.Unit.GIB), is(BigDecimal.valueOf(1073741824L))); + assertThat(size.to(Size.Unit.TIB), is(BigDecimal.valueOf(1048576L))); + assertThat(size.to(Size.Unit.PIB), is(BigDecimal.valueOf(1024L))); + assertThat(size.to(Size.Unit.EIB), is(BigDecimal.valueOf(1L))); + + assertThat(size.toString(Size.UnitKind.BINARY_UPPER_CASE), is("1 EB")); + assertThat(size.toString(Size.UnitKind.BINARY_BI), is("1 EiB")); + assertThat(size.toString(Size.Unit.GIB, Size.UnitKind.BINARY_BI), is("1073741824 GiB")); + assertThat(size.toString(Size.Unit.GIB, Size.UnitKind.BINARY_UPPER_CASE), is("1073741824 GB")); + } + + @Test + void testConversionsDecimal() { + Size size = Size.create(1048576, Size.Unit.BYTE); + + assertThat(size.toBytes(), is(1048576L)); + + assertThat(size.to(Size.Unit.BYTE), is(BigDecimal.valueOf(1048576L))); + assertThat(size.to(Size.Unit.KB), is(new BigDecimal("1048.576"))); + assertThat(size.to(Size.Unit.MB), is(new BigDecimal("1.048576"))); + assertThat(size.to(Size.Unit.GB), closeTo(new BigDecimal("0.001048576"), BigDecimal.ZERO)); + assertThat(size.to(Size.Unit.TB), closeTo(new BigDecimal("0.000001048576"), BigDecimal.ZERO)); + assertThat(size.to(Size.Unit.PB), closeTo(new BigDecimal("0.000000001048576"), BigDecimal.ZERO)); + assertThat(size.to(Size.Unit.EB), closeTo(new BigDecimal("0.000000000001048576"), BigDecimal.ZERO)); + + assertThat(size.toString(), is("1048576 B")); + assertThat(size.toString(Size.UnitKind.DECIMAL_LOWER_CASE), is("1048576 b")); + assertThat(size.toString(Size.Unit.EB, Size.UnitKind.DECIMAL_UPPER_CASE), is("0.000000000001048576 eB")); + } + + @Test + void testParsingDecimal() { + testParsing("10", 10); + testParsing("2 kb", 2_000); + testParsing("2 kB", 2_000); + testParsing("3 mB", 3_000_000); + testParsing("3 mb", 3_000_000); + testParsing("4 gB", 4_000_000_000L); + testParsing("4 gb", 4_000_000_000L); + testParsing("7 tB", 7_000_000_000_000L); + testParsing("7 tb", 7_000_000_000_000L); + testParsing("5 pB", 5_000_000_000_000_000L); + testParsing("5 pb", 5_000_000_000_000_000L); + testParsing("6 eB", 6_000_000_000_000_000_000L); + testParsing("6 eb", 6_000_000_000_000_000_000L); + + testParsing("2.42 kb", 2_420); + testParsing("2.42 kB", 2_420); + testParsing("3.42 mB", 3_420_000); + testParsing("3.42 mb", 3_420_000); + testParsing("4.42 gB", 4_420_000_000L); + testParsing("4.42 gb", 4_420_000_000L); + testParsing("7.42 tB", 7_420_000_000_000L); + testParsing("7.42 tb", 7_420_000_000_000L); + testParsing("5.42 pB", 5_420_000_000_000_000L); + testParsing("5.42 pb", 5_420_000_000_000_000L); + testParsing("6.42 eB", 6_420_000_000_000_000_000L); + testParsing("6.42 eb", 6_420_000_000_000_000_000L); + } + + @Test + void testParsingBinary() { + testParsing("10", 10); + testParsing("2 KB", 2_048); + testParsing("2 KiB", 2_048); + testParsing("3 MB", 3_145_728); + testParsing("3 MiB", 3_145_728); + testParsing("4 GB", 4_294_967_296L); + testParsing("4 GiB", 4_294_967_296L); + testParsing("7 TB", 7_696_581_394_432L); + testParsing("7 TiB", 7_696_581_394_432L); + testParsing("5 PB", 5_629_499_534_213_120L); + testParsing("5 PiB", 5_629_499_534_213_120L); + testParsing("6 EB", 6_917_529_027_641_081_856L); + testParsing("6 EiB", 6_917_529_027_641_081_856L); + + testParsing("3.5 KB", 3_584); + testParsing("3.5 KiB", 3_584); + // not testing others, as this combines decimal numbers with binary numbers + } + + private void testParsing(String value, long numberOfBytes) { + Size size = Size.parse(value); + assertThat(size.toBytes(), is(numberOfBytes)); + } +} + diff --git a/common/concurrency/limits/README.md b/common/concurrency/limits/README.md new file mode 100644 index 00000000000..c17184f10ff --- /dev/null +++ b/common/concurrency/limits/README.md @@ -0,0 +1,37 @@ +Concurrency Limits +----- + +This module provides concurrency limits, so we can limit the number of concurrent, in-progress operations (for example in WebServer). + +The implemented concurrency limits are: + +| Key | Weight | Description | +|---------|--------|--------------------------------------------------------------| +| `fixed` | `90` | Semaphore based concurrency limit, supports queueing | +| `aimd` | `80` | AIMD based limit (additive-increase/multiplicative-decrease) | + +Current usage: `helidon-webserver` + +The weight is not significant (unless you want to override an implementation using your own Limit with a higher weight), as the usages in Helidon use a single (optional) implementation that must be correctly typed in +configuration. + +# Fixed concurrency limit + +The fixed concurrency limit is based on a semaphore behavior. +You can define the number of available permits, then each time a token is requested, a permit (if available) is returned. +When the token is finished (through one of its lifecycle operations), the permit is returned. + +When the limit is set to 0, an unlimited implementation is used. + +The fixed limit also provides support for defining a queue. If set to a value above `0`, queuing is enabled. In such a case we enqueue a certain number of requests (with a configurable timeout). + +Defaults are: +- `permits: 0` - unlimited permits (no limit) +- `queue-length: 0` - no queuing +- `queue-timeout: PT1S` - 1 second timout in queue, if queuing is enabled + +# AIMD concurrency limit + +The additive-increase/multiplicative-decrease (AIMD) algorithm is a feedback control algorithm best known for its use in TCP congestion control. AIMD combines linear growth of the congestion window when there is no congestion with an exponential reduction when congestion is detected. + +This implementation provides variable concurrency limit with fixed minimal/maximal number of permits. diff --git a/common/concurrency/limits/pom.xml b/common/concurrency/limits/pom.xml new file mode 100644 index 00000000000..79f981da434 --- /dev/null +++ b/common/concurrency/limits/pom.xml @@ -0,0 +1,127 @@ + + + + + 4.0.0 + + io.helidon.common.concurrency + helidon-common-concurrency-project + 4.2.0-SNAPSHOT + ../pom.xml + + + helidon-common-concurrency-limits + Helidon Common Concurrency Limits + + + + io.helidon.common + helidon-common + + + io.helidon.builder + helidon-builder-api + + + io.helidon.common + helidon-common-config + + + io.helidon.service + helidon-service-registry + true + + + io.helidon.config + helidon-config + test + + + io.helidon.config + helidon-config-yaml + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimit.java new file mode 100644 index 00000000000..1c901e8b78c --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimit.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.config.Config; + +/** + * AIMD based limiter. + *

    + * The additive-increase/multiplicative-decrease (AIMD) algorithm is a feedback control algorithm best known for its use in TCP + * congestion control. AIMD combines linear growth of the congestion window when there is no congestion with an exponential + * reduction when congestion is detected. + */ +@SuppressWarnings("removal") +@RuntimeType.PrototypedBy(AimdLimitConfig.class) +public class AimdLimit implements Limit, SemaphoreLimit, RuntimeType.Api { + static final String TYPE = "aimd"; + + private final AimdLimitConfig config; + private final AimdLimitImpl aimdLimitImpl; + + private AimdLimit(AimdLimitConfig config) { + this.config = config; + this.aimdLimitImpl = new AimdLimitImpl(config); + } + + /** + * Create a new fluent API builder to construct {@link io.helidon.common.concurrency.limits.AimdLimit} + * instance. + * + * @return fluent API builder + */ + public static AimdLimitConfig.Builder builder() { + return AimdLimitConfig.builder(); + } + + /** + * Create a new instance with all defaults. + * + * @return a new limit instance + */ + public static AimdLimit create() { + return builder().build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the AIMD limit + * @return a new limit instance configured from {@code config} + */ + public static AimdLimit create(Config config) { + return builder() + .config(config) + .build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the AIMD limit + * @return a new limit instance configured from {@code config} + */ + public static AimdLimit create(AimdLimitConfig config) { + return new AimdLimit(config); + } + + /** + * Create a new instance customizing its configuration. + * + * @param consumer consumer of configuration builder + * @return a new limit instance configured from the builder + */ + public static AimdLimit create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + @Override + public T invoke(Callable callable) throws Exception { + return aimdLimitImpl.invoke(callable); + } + + @Override + public void invoke(Runnable runnable) throws Exception { + aimdLimitImpl.invoke(runnable); + } + + @Override + public Optional tryAcquire(boolean wait) { + return aimdLimitImpl.tryAcquire(); + } + + @SuppressWarnings("removal") + @Override + public Semaphore semaphore() { + return aimdLimitImpl.semaphore(); + } + + @Override + public String name() { + return config.name(); + } + + @Override + public String type() { + return TYPE; + } + + @Override + public AimdLimitConfig prototype() { + return config; + } + + @Override + public Limit copy() { + return config.build(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitConfigBlueprint.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitConfigBlueprint.java new file mode 100644 index 00000000000..400fbb99682 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitConfigBlueprint.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.concurrency.limits.spi.LimitProvider; + +/** + * Configuration of {@link io.helidon.common.concurrency.limits.AimdLimit}. + */ +@Prototype.Blueprint +@Prototype.Configured(value = AimdLimit.TYPE, root = false) +@Prototype.Provides(LimitProvider.class) +interface AimdLimitConfigBlueprint extends Prototype.Factory { + /** + * Backoff ratio to use for the algorithm. + * The value must be within [0.5, 1.0). + * + * @return backoff ratio + */ + @Option.Configured + @Option.DefaultDouble(0.9) + double backoffRatio(); + + /** + * Initial limit. + * The value must be within [{@link #minLimit()}, {@link #maxLimit()}]. + * + * @return initial limit + */ + @Option.Configured + @Option.DefaultInt(20) + int initialLimit(); + + /** + * Maximal limit. + * The value must be same or higher than {@link #minLimit()}. + * + * @return maximal limit + */ + @Option.Configured + @Option.DefaultInt(200) + int maxLimit(); + + /** + * Minimal limit. + * The value must be same or lower than {@link #maxLimit()}. + * + * @return minimal limit + */ + @Option.Configured + @Option.DefaultInt(20) + int minLimit(); + + /** + * Timeout that when exceeded is the same as if the task failed. + * + * @return task timeout, defaults to 5 seconds + */ + @Option.Configured + @Option.Default("PT5S") + Duration timeout(); + + /** + * A clock that supplies nanosecond time. + * + * @return supplier of current nanoseconds, defaults to {@link java.lang.System#nanoTime()} + */ + Optional> clock(); + + /** + * Name of this instance. + * + * @return name of the instance + */ + @Option.Default(AimdLimit.TYPE) + String name(); +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitImpl.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitImpl.java new file mode 100644 index 00000000000..2431ec6e24c --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitImpl.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.io.Serial; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import io.helidon.common.config.ConfigException; + +class AimdLimitImpl { + private final double backoffRatio; + private final long timeoutInNanos; + private final int minLimit; + private final int maxLimit; + + private final Supplier clock; + private final AtomicInteger concurrentRequests; + private final AdjustableSemaphore semaphore; + + private final AtomicInteger limit; + private final Lock limitLock = new ReentrantLock(); + + AimdLimitImpl(AimdLimitConfig config) { + int initialLimit = config.initialLimit(); + this.backoffRatio = config.backoffRatio(); + this.timeoutInNanos = config.timeout().toNanos(); + this.minLimit = config.minLimit(); + this.maxLimit = config.maxLimit(); + this.clock = config.clock().orElseGet(() -> System::nanoTime); + + this.concurrentRequests = new AtomicInteger(); + this.semaphore = new AdjustableSemaphore(initialLimit); + + this.limit = new AtomicInteger(initialLimit); + + if (!(backoffRatio < 1.0 && backoffRatio >= 0.5)) { + throw new ConfigException("Backoff ratio must be within [0.5, 1.0)"); + } + if (maxLimit < minLimit) { + throw new ConfigException("Max limit must be higher than min limit, or equal to it"); + } + if (initialLimit > maxLimit) { + throw new ConfigException("Initial limit must be lower than max limit, or equal to it"); + } + if (initialLimit < minLimit) { + throw new ConfigException("Initial limit must be higher than minimum limit, or equal to it"); + } + } + + Semaphore semaphore() { + return semaphore; + } + + int currentLimit() { + return limit.get(); + } + + Optional tryAcquire() { + if (!semaphore.tryAcquire()) { + return Optional.empty(); + } + + return Optional.of(new AimdToken(clock, concurrentRequests)); + } + + void invoke(Runnable runnable) throws Exception { + invoke(() -> { + runnable.run(); + return null; + }); + } + + T invoke(Callable callable) throws Exception { + long startTime = clock.get(); + int currentRequests = concurrentRequests.incrementAndGet(); + + if (semaphore.tryAcquire()) { + try { + T response = callable.call(); + updateWithSample(startTime, clock.get(), currentRequests, true); + return response; + } catch (IgnoreTaskException e) { + return e.handle(); + } catch (Throwable e) { + updateWithSample(startTime, clock.get(), currentRequests, false); + throw e; + } finally { + concurrentRequests.decrementAndGet(); + semaphore.release(); + } + } else { + throw new LimitException("No more permits available for the semaphore"); + } + } + + void updateWithSample(long startTime, long endTime, int currentRequests, boolean success) { + long rtt = endTime - startTime; + + int currentLimit = limit.get(); + if (rtt > timeoutInNanos || !success) { + currentLimit = (int) (currentLimit * backoffRatio); + } else if (currentRequests * 2 >= currentLimit) { + currentLimit = currentLimit + 1; + } + setLimit(Math.min(maxLimit, Math.max(minLimit, currentLimit))); + } + + private void setLimit(int newLimit) { + if (newLimit == limit.get()) { + // already have the correct limit + return; + } + // now we lock, to do this only once in parallel, + // as otherwise we may end up in strange lands + limitLock.lock(); + try { + int oldLimit = limit.get(); + if (oldLimit == newLimit) { + // parallel thread already fixed it + return; + } + limit.set(newLimit); + + if (newLimit > oldLimit) { + this.semaphore.release(newLimit - oldLimit); + } else { + this.semaphore.reducePermits(oldLimit - newLimit); + } + } finally { + limitLock.unlock(); + } + } + + private static final class AdjustableSemaphore extends Semaphore { + @Serial + private static final long serialVersionUID = 114L; + + private AdjustableSemaphore(int permits) { + super(permits); + } + + @Override + protected void reducePermits(int reduction) { + super.reducePermits(reduction); + } + } + + private class AimdToken implements Limit.Token { + private final long startTime; + private final int currentRequests; + + private AimdToken(Supplier clock, AtomicInteger concurrentRequests) { + startTime = clock.get(); + currentRequests = concurrentRequests.incrementAndGet(); + } + + @Override + public void dropped() { + try { + updateWithSample(startTime, clock.get(), currentRequests, false); + } finally { + AimdLimitImpl.this.semaphore.release(); + } + } + + @Override + public void ignore() { + concurrentRequests.decrementAndGet(); + AimdLimitImpl.this.semaphore.release(); + } + + @Override + public void success() { + try { + updateWithSample(startTime, clock.get(), currentRequests, true); + concurrentRequests.decrementAndGet(); + } finally { + AimdLimitImpl.this.semaphore.release(); + } + } + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitProvider.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitProvider.java new file mode 100644 index 00000000000..ae0af21e0ab --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import io.helidon.common.Weight; +import io.helidon.common.concurrency.limits.spi.LimitProvider; +import io.helidon.common.config.Config; + +/** + * {@link java.util.ServiceLoader} service provider for {@link io.helidon.common.concurrency.limits.AimdLimit} + * limit implementation. + */ +@Weight(80) +public class AimdLimitProvider implements LimitProvider { + /** + * Constructor required by the service loader. + */ + public AimdLimitProvider() { + } + + @Override + public String configKey() { + return AimdLimit.TYPE; + } + + @Override + public Limit create(Config config, String name) { + return AimdLimit.builder() + .config(config) + .name(name) + .build(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimit.java new file mode 100644 index 00000000000..97255156e4e --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimit.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.config.Config; + +/** + * Semaphore based limit, that supports queuing for a permit, and timeout on the queue. + * The default behavior is non-queuing. + * + * @see io.helidon.common.concurrency.limits.FixedLimitConfig + */ +@SuppressWarnings("removal") +@RuntimeType.PrototypedBy(FixedLimitConfig.class) +public class FixedLimit implements Limit, SemaphoreLimit, RuntimeType.Api { + /** + * Default limit, meaning unlimited execution. + */ + public static final int DEFAULT_LIMIT = 0; + /** + * Default length of the queue. + */ + public static final int DEFAULT_QUEUE_LENGTH = 0; + /** + * Timeout of a request that is enqueued. + */ + public static final String DEFAULT_QUEUE_TIMEOUT_DURATION = "PT1S"; + + static final String TYPE = "fixed"; + + private final FixedLimitConfig config; + private final LimiterHandler handler; + private final int initialPermits; + + private FixedLimit(FixedLimitConfig config) { + this.config = config; + if (config.permits() == 0 && config.semaphore().isEmpty()) { + this.handler = new NoOpSemaphoreHandler(); + this.initialPermits = 0; + } else { + Semaphore semaphore = config.semaphore().orElseGet(() -> new Semaphore(config.permits(), config.fair())); + this.initialPermits = semaphore.availablePermits(); + if (config.queueLength() == 0) { + this.handler = new RealSemaphoreHandler(semaphore); + } else { + this.handler = new QueuedSemaphoreHandler(semaphore, + config.queueLength(), + config.queueTimeout()); + } + } + } + + /** + * Create a new fluent API builder to construct {@link FixedLimit} + * instance. + * + * @return fluent API builder + */ + public static FixedLimitConfig.Builder builder() { + return FixedLimitConfig.builder(); + } + + /** + * Create a new instance with all defaults (no limit). + * + * @return a new limit instance + */ + public static FixedLimit create() { + return builder().build(); + } + + /** + * Create an instance from the provided semaphore. + * + * @param semaphore semaphore to use + * @return a new fixed limit backed by the provided semaphore + */ + public static FixedLimit create(Semaphore semaphore) { + return builder() + .semaphore(semaphore) + .build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the fixed limit + * @return a new limit instance configured from {@code config} + */ + public static FixedLimit create(Config config) { + return builder() + .config(config) + .build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the fixed limit + * @return a new limit instance configured from {@code config} + */ + public static FixedLimit create(FixedLimitConfig config) { + return new FixedLimit(config); + } + + /** + * Create a new instance customizing its configuration. + * + * @param consumer consumer of configuration builder + * @return a new limit instance configured from the builder + */ + public static FixedLimit create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + @Override + public T invoke(Callable callable) throws Exception { + return handler.invoke(callable); + } + + @Override + public void invoke(Runnable runnable) throws Exception { + handler.invoke(runnable); + } + + @Override + public Optional tryAcquire(boolean wait) { + return handler.tryAcquire(wait); + } + + @SuppressWarnings("removal") + @Override + public Semaphore semaphore() { + return handler.semaphore(); + } + + @Override + public FixedLimitConfig prototype() { + return config; + } + + @Override + public String name() { + return config.name(); + } + + @Override + public String type() { + return FixedLimit.TYPE; + } + + @Override + public Limit copy() { + if (config.semaphore().isPresent()) { + Semaphore semaphore = config.semaphore().get(); + + return FixedLimitConfig.builder() + .from(config) + .semaphore(new Semaphore(initialPermits, semaphore.isFair())) + .build(); + } + return config.build(); + } + + @SuppressWarnings("removal") + private interface LimiterHandler extends SemaphoreLimit, LimitAlgorithm { + } + + private static class NoOpSemaphoreHandler implements LimiterHandler { + private static final Token TOKEN = new Token() { + @Override + public void dropped() { + } + + @Override + public void ignore() { + } + + @Override + public void success() { + } + }; + + @Override + public T invoke(Callable callable) throws Exception { + try { + return callable.call(); + } catch (IgnoreTaskException e) { + return e.handle(); + } + } + + @Override + public void invoke(Runnable runnable) { + runnable.run(); + } + + @Override + public Optional tryAcquire(boolean wait) { + return Optional.of(TOKEN); + } + + @SuppressWarnings("removal") + @Override + public Semaphore semaphore() { + return NoopSemaphore.INSTANCE; + } + } + + @SuppressWarnings("removal") + private static class RealSemaphoreHandler implements LimiterHandler { + private final Semaphore semaphore; + + private RealSemaphoreHandler(Semaphore semaphore) { + this.semaphore = semaphore; + } + + @Override + public T invoke(Callable callable) throws Exception { + if (semaphore.tryAcquire()) { + try { + return callable.call(); + } catch (IgnoreTaskException e) { + return e.handle(); + } finally { + semaphore.release(); + } + } else { + throw new LimitException("No more permits available for the semaphore"); + } + } + + @Override + public void invoke(Runnable runnable) throws Exception { + if (semaphore.tryAcquire()) { + try { + runnable.run(); + } catch (IgnoreTaskException e) { + e.handle(); + } finally { + semaphore.release(); + } + } else { + throw new LimitException("No more permits available for the semaphore"); + } + } + + @Override + public Optional tryAcquire(boolean wait) { + if (!semaphore.tryAcquire()) { + return Optional.empty(); + } + return Optional.of(new SemaphoreToken(semaphore)); + } + + @Override + public Semaphore semaphore() { + return semaphore; + } + } + + private static class QueuedSemaphoreHandler implements LimiterHandler { + private final Semaphore semaphore; + private final int queueLength; + private final long timeoutMillis; + + private QueuedSemaphoreHandler(Semaphore semaphore, int queueLength, Duration queueTimeout) { + this.semaphore = semaphore; + this.queueLength = queueLength; + this.timeoutMillis = queueTimeout.toMillis(); + } + + @Override + public Optional tryAcquire(boolean wait) { + if (semaphore.getQueueLength() >= this.queueLength) { + // this is an estimate - we do not promise to be precise here + return Optional.empty(); + } + + try { + if (wait) { + if (!semaphore.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)) { + return Optional.empty(); + } + } else { + if (!semaphore.tryAcquire()) { + return Optional.empty(); + } + } + + } catch (InterruptedException e) { + return Optional.empty(); + } + return Optional.of(new SemaphoreToken(semaphore)); + } + + @Override + public Semaphore semaphore() { + return semaphore; + } + } + + private static class SemaphoreToken implements Token { + private final Semaphore semaphore; + + private SemaphoreToken(Semaphore semaphore) { + this.semaphore = semaphore; + } + + @Override + public void dropped() { + semaphore.release(); + } + + @Override + public void ignore() { + semaphore.release(); + } + + @Override + public void success() { + semaphore.release(); + } + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitConfigBlueprint.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitConfigBlueprint.java new file mode 100644 index 00000000000..c5e672c70cd --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitConfigBlueprint.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Semaphore; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.concurrency.limits.spi.LimitProvider; + +/** + * Configuration of {@link FixedLimit}. + * + * @see #permits() + * @see #queueLength() + * @see #queueTimeout() + */ +@Prototype.Blueprint +@Prototype.Configured(value = FixedLimit.TYPE, root = false) +@Prototype.Provides(LimitProvider.class) +interface FixedLimitConfigBlueprint extends Prototype.Factory { + /** + * Number of permit to allow. + * Defaults to {@value FixedLimit#DEFAULT_LIMIT}. + * When set to {@code 0}, we switch to unlimited. + * + * @return number of permits + */ + @Option.Configured + @Option.DefaultInt(FixedLimit.DEFAULT_LIMIT) + int permits(); + + /** + * Whether the {@link java.util.concurrent.Semaphore} should be {@link java.util.concurrent.Semaphore#isFair()}. + * Defaults to {@code false}. + * + * @return whether this should be a fair semaphore + */ + @Option.Configured + @Option.DefaultBoolean(false) + boolean fair(); + + /** + * How many requests can be enqueued waiting for a permit. + * Note that this may not be an exact behavior due to concurrent invocations. + * We use {@link java.util.concurrent.Semaphore#getQueueLength()} in the + * {@link io.helidon.common.concurrency.limits.FixedLimit} implementation. + * Default value is {@value FixedLimit#DEFAULT_QUEUE_LENGTH}. + * If set to {code 0}, there is no queueing. + * + * @return number of requests to enqueue + */ + @Option.Configured + @Option.DefaultInt(FixedLimit.DEFAULT_QUEUE_LENGTH) + int queueLength(); + + /** + * How long to wait for a permit when enqueued. + * Defaults to {@value FixedLimit#DEFAULT_QUEUE_TIMEOUT_DURATION} + * + * @return duration of the timeout + */ + @Option.Configured + @Option.Default(FixedLimit.DEFAULT_QUEUE_TIMEOUT_DURATION) + Duration queueTimeout(); + + /** + * Name of this instance. + * + * @return name of the instance + */ + @Option.Default(FixedLimit.TYPE) + String name(); + + /** + * Explicitly configured semaphore. + * Note that if this is set, all other configuration is ignored. + * + * @return semaphore instance + */ + Optional semaphore(); + +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitProvider.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitProvider.java new file mode 100644 index 00000000000..2d221579bf1 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import io.helidon.common.Weight; +import io.helidon.common.concurrency.limits.spi.LimitProvider; +import io.helidon.common.config.Config; + +/** + * {@link java.util.ServiceLoader} service provider for {@link FixedLimit} + * limit implementation. + */ +@Weight(90) +public class FixedLimitProvider implements LimitProvider { + /** + * Constructor required by the service loader. + */ + public FixedLimitProvider() { + } + + @Override + public String configKey() { + return FixedLimit.TYPE; + } + + @Override + public Limit create(Config config, String name) { + return FixedLimit.builder() + .config(config) + .name(name) + .build(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/IgnoreTaskException.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/IgnoreTaskException.java new file mode 100644 index 00000000000..e6e7458fb89 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/IgnoreTaskException.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Objects; + +/** + * If this exception is thrown from a limited task within + * {@link Limit#invoke(java.util.concurrent.Callable)}, the + * invocation will be ignored by possible algorithms (for example when considering round-trip timing). + *

    + * This should be used for cases where we never got to execute the intended task. + * This exception should never be thrown by {@link Limit}, it should always + * be translated to a proper return type, or actual exception. + */ +public class IgnoreTaskException extends RuntimeException { + /** + * Desired return value, if we want to ignore the result, yet we still provide valid response. + */ + private final Object returnValue; + /** + * Exception to throw to the user. This is to allow throwing an exception while ignoring it for limits algorithm. + */ + private final Exception exception; + + /** + * Create a new instance with a cause. + * + * @param cause the cause of this exception + */ + public IgnoreTaskException(Exception cause) { + super(Objects.requireNonNull(cause)); + + this.exception = cause; + this.returnValue = null; + } + + /** + * Create a new instance with a return value. + * + * @param returnValue value to return, even though this invocation should be ignored + * return value may be {@code null}. + */ + public IgnoreTaskException(Object returnValue) { + this.exception = null; + this.returnValue = returnValue; + } + + /** + * This is used by limit implementations to either return the value, or throw an exception. + * + * @return the value provided to be the return value + * @param type of the return value + * @throws Exception exception provided by the task + */ + @SuppressWarnings("unchecked") + public T handle() throws Exception { + if (returnValue == null && exception != null) { + throw exception; + } + return (T) returnValue; + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/Limit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/Limit.java new file mode 100644 index 00000000000..fc375762006 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/Limit.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import io.helidon.common.config.NamedService; +import io.helidon.service.registry.Service; + +/** + * Contract for a concurrency limiter. + */ +@Service.Contract +public interface Limit extends LimitAlgorithm, NamedService { + /** + * Create a copy of this limit with the same configuration. + * + * @return a copy of this limit + */ + Limit copy(); +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitAlgorithm.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitAlgorithm.java new file mode 100644 index 00000000000..c81761abb65 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitAlgorithm.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Optional; +import java.util.concurrent.Callable; + +/** + * Concurrency limit algorithm. + *

    + * There are two options how to use a limit - by handling a token provided by {@link #tryAcquire()}, + * or by invoking a callable or runnable through one of the invoke methods (such as {@link #invoke(Runnable)}. + *

    + * The invoke methods are backed by the same {@link #tryAcquire()} methods, so behavior is consistent. + */ +public interface LimitAlgorithm { + /** + * Invoke a callable within the limits of this limiter. + *

    + * {@link io.helidon.common.concurrency.limits.Limit} implementors note: + * Make sure to catch {@link io.helidon.common.concurrency.limits.IgnoreTaskException} from the + * callable, and call its {@link IgnoreTaskException#handle()} to either return the provided result, + * or throw the exception after ignoring the timing for future decisions. + * + * @param callable callable to execute within the limit + * @param the callable return type + * @return result of the callable + * @throws LimitException in case the limiter did not have an available permit + * @throws java.lang.Exception in case the task failed with an exception + */ + default T invoke(Callable callable) throws LimitException, Exception { + Optional token = tryAcquire(); + if (token.isEmpty()) { + throw new LimitException("No token available."); + } + Token permit = token.get(); + try { + T response = callable.call(); + permit.success(); + return response; + } catch (IgnoreTaskException e) { + permit.ignore(); + return e.handle(); + } catch (Exception e) { + permit.dropped(); + throw e; + } + } + + /** + * Invoke a runnable within the limits of this limiter. + *

    + * {@link io.helidon.common.concurrency.limits.Limit} implementors note: + * Make sure to catch {@link io.helidon.common.concurrency.limits.IgnoreTaskException} from the + * runnable, and call its {@link IgnoreTaskException#handle()} to either return the provided result, + * or throw the exception after ignoring the timing for future decisions. + * + * @param runnable runnable to execute within the limit + * @throws LimitException in case the limiter did not have an available permit + * @throws java.lang.Exception in case the task failed with an exception + */ + default void invoke(Runnable runnable) throws LimitException, Exception { + Optional token = tryAcquire(); + if (token.isEmpty()) { + throw new LimitException("No token available."); + } + Token permit = token.get(); + try { + runnable.run(); + permit.success(); + } catch (IgnoreTaskException e) { + permit.ignore(); + e.handle(); + } catch (Exception e) { + permit.dropped(); + throw e; + } + } + + /** + * Try to acquire a token, waiting for available permits for the configured amount of time, if queuing is enabled. + *

    + * If acquired, the caller must call one of the {@link io.helidon.common.concurrency.limits.Limit.Token} + * operations to release the token. + * If the response is empty, the limit does not have an available token. + * + * @return acquired token, or empty if there is no available token + */ + default Optional tryAcquire() { + return tryAcquire(true); + } + + /** + * Try to acquire a token, waiting for available permits for the configured amount of time, if + * {@code wait} is enabled, returning immediately otherwise. + *

    + * If acquired, the caller must call one of the {@link io.helidon.common.concurrency.limits.Limit.Token} + * operations to release the token. + * If the response is empty, the limit does not have an available token. + * + * @param wait whether to wait in the queue (if one is configured/available in the limit), or to return immediately + * @return acquired token, or empty if there is no available token + */ + Optional tryAcquire(boolean wait); + + /** + * When a token is retrieved from {@link #tryAcquire()}, one of its methods must be called when the task + * is over, to release the token back to the pool (such as a permit returned to a {@link java.util.concurrent.Semaphore}). + *

    + * Choice of method to invoke may influence the algorithm used for determining number of available permits. + */ + interface Token { + /** + * Operation was dropped, for example because it hit a timeout, or was rejected by other limits. + * Loss based {@link io.helidon.common.concurrency.limits.Limit} implementations will likely do an aggressive + * reducing in limit when this happens. + */ + void dropped(); + + /** + * The operation failed before any meaningful RTT measurement could be made and should be ignored to not + * introduce an artificially low RTT. + */ + void ignore(); + + /** + * Notification that the operation succeeded and internally measured latency should be used as an RTT sample. + */ + void success(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitException.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitException.java new file mode 100644 index 00000000000..987d9cf99fa --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Objects; + +/** + * A limit was reached and the submitted task cannot be executed. + * + * @see io.helidon.common.concurrency.limits.Limit#invoke(java.util.concurrent.Callable) + * @see io.helidon.common.concurrency.limits.Limit#invoke(Runnable) + */ +public class LimitException extends RuntimeException { + /** + * A new limit exception with a cause. + * + * @param cause cause of the limit reached + */ + public LimitException(Exception cause) { + super(Objects.requireNonNull(cause)); + } + + /** + * A new limit exception with a message. + * + * @param message description of why the limit was reached + */ + public LimitException(String message) { + super(Objects.requireNonNull(message)); + } +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/NoopSemaphore.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/NoopSemaphore.java similarity index 79% rename from webserver/webserver/src/main/java/io/helidon/webserver/NoopSemaphore.java rename to common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/NoopSemaphore.java index a20c8790089..b86210e0f77 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/NoopSemaphore.java +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/NoopSemaphore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,28 @@ * limitations under the License. */ -package io.helidon.webserver; +package io.helidon.common.concurrency.limits; import java.util.Collection; import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -/* +/** * A semaphore that does nothing. + * Use {@link #INSTANCE} to get an instance of this semaphore. + * + * @deprecated this is only provided for backward compatibility and will be removed, use + * {@link FixedLimit#create()} to get unlimited limit */ -class NoopSemaphore extends Semaphore { - NoopSemaphore() { +@Deprecated(forRemoval = true, since = "4.2.0") +public class NoopSemaphore extends Semaphore { + /** + * Singleton instance to be used whenever needed. + */ + public static final Semaphore INSTANCE = new NoopSemaphore(); + + private NoopSemaphore() { super(0); } diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/SemaphoreLimit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/SemaphoreLimit.java new file mode 100644 index 00000000000..77332ce397b --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/SemaphoreLimit.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.concurrent.Semaphore; + +/** + * The {@link io.helidon.common.concurrency.limits.Limit} is backed by a semaphore, and this provides + * direct access to the semaphore. + * Note that this usage may bypass calculation of limits if the semaphore is used directly. + * This is for backward compatibility only, and will be removed. + * + * @deprecated DO NOT USE except for backward compatibility with semaphore based handling + */ +@Deprecated(since = "4.2.0", forRemoval = true) +public interface SemaphoreLimit { + /** + * Underlying semaphore of this limit. + * + * @return the semaphore instance + * @deprecated this only exists for backward compatibility of Helidon WebServer and will be removed + */ + @Deprecated(forRemoval = true, since = "4.2.0") + Semaphore semaphore(); +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/package-info.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/package-info.java new file mode 100644 index 00000000000..2804c1ef810 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Concurrency limits API and default implementations. + * + * @see io.helidon.common.concurrency.limits.Limit + * @see io.helidon.common.concurrency.limits.FixedLimit + */ +package io.helidon.common.concurrency.limits; diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/LimitProvider.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/LimitProvider.java new file mode 100644 index 00000000000..c192b734ff9 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/LimitProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits.spi; + +import io.helidon.common.concurrency.limits.Limit; +import io.helidon.common.config.ConfiguredProvider; +import io.helidon.service.registry.Service; + +/** + * A {@link java.util.ServiceLoader} (and service registry) service provider to discover rate limits. + */ +@Service.Contract +public interface LimitProvider extends ConfiguredProvider { +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/package-info.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/package-info.java new file mode 100644 index 00000000000..e88d31d397d --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Extension points to create custom concurrency rate limits. + */ +package io.helidon.common.concurrency.limits.spi; diff --git a/common/concurrency/limits/src/main/java/module-info.java b/common/concurrency/limits/src/main/java/module-info.java new file mode 100644 index 00000000000..f1b23377399 --- /dev/null +++ b/common/concurrency/limits/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Concurrency limits. + * + * @see io.helidon.common.concurrency.limits + */ +module io.helidon.common.concurrency.limits { + requires static io.helidon.service.registry; + + requires io.helidon.builder.api; + requires io.helidon.common; + requires io.helidon.common.config; + + exports io.helidon.common.concurrency.limits; + exports io.helidon.common.concurrency.limits.spi; + + provides io.helidon.common.concurrency.limits.spi.LimitProvider + with io.helidon.common.concurrency.limits.FixedLimitProvider, + io.helidon.common.concurrency.limits.AimdLimitProvider; +} \ No newline at end of file diff --git a/common/concurrency/limits/src/main/resources/META-INF/helidon/service.loader b/common/concurrency/limits/src/main/resources/META-INF/helidon/service.loader new file mode 100644 index 00000000000..e2dd011e9fa --- /dev/null +++ b/common/concurrency/limits/src/main/resources/META-INF/helidon/service.loader @@ -0,0 +1,2 @@ +# List of service contracts we want to support either from service registry, or from service loader +io.helidon.common.concurrency.limits.spi.LimitProvider diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/AimdLimitTest.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/AimdLimitTest.java new file mode 100644 index 00000000000..c118cce5565 --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/AimdLimitTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +public class AimdLimitTest { + @Test + void decreaseOnDrops() { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(30) + .buildPrototype(); + + AimdLimitImpl limiter = new AimdLimitImpl(config); + + assertThat(limiter.currentLimit(), is(30)); + limiter.updateWithSample(0, 0, 0, false); + assertThat(limiter.currentLimit(), is(27)); + } + + @Test + void decreaseOnTimeoutExceeded() { + Duration timeout = Duration.ofSeconds(1); + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(30) + .timeout(timeout) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + limiter.updateWithSample(0, timeout.toNanos() + 1, 0, true); + assertThat(limiter.currentLimit(), is(27)); + } + + @Test + void increaseOnSuccess() { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(20) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + limiter.updateWithSample(0, Duration.ofMillis(1).toNanos(), 10, true); + assertThat(limiter.currentLimit(), is(21)); + } + + @Test + void successOverflow() { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(21) + .maxLimit(21) + .minLimit(0) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + limiter.updateWithSample(0, Duration.ofMillis(1).toNanos(), 10, true); + // after success limit should still be at the max. + assertThat(limiter.currentLimit(), is(21)); + } + + @Test + void testDefault() { + AimdLimitConfig config = AimdLimitConfig.builder() + .minLimit(10) + .initialLimit(10) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + assertThat(limiter.currentLimit(), is(10)); + } + + @Test + void concurrentUpdatesAndReads() throws InterruptedException { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(1) + .backoffRatio(0.9) + .timeout(Duration.ofMillis(100)) + .minLimit(1) + .maxLimit(200) + .buildPrototype(); + AimdLimitImpl limit = new AimdLimitImpl(config); + + int threadCount = 100; + int operationsPerThread = 1_000; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + AtomicInteger successCount = new AtomicInteger(0); + AtomicInteger timeoutCount = new AtomicInteger(0); + AtomicInteger dropCount = new AtomicInteger(0); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + startLatch.await(); // Wait for all threads to be ready + for (int j = 0; j < operationsPerThread; j++) { + long startTime = System.nanoTime(); + long rtt = (long) (Math.random() * 200_000_000); // 0-200ms + int concurrentRequests = (int) (Math.random() * limit.currentLimit() * 2); + boolean didDrop = Math.random() < 0.01; // 1% chance of drop + + limit.updateWithSample(startTime, rtt, concurrentRequests, !didDrop); + + if (didDrop) { + dropCount.incrementAndGet(); + } else if (rtt > config.timeout().toNanos()) { + timeoutCount.incrementAndGet(); + } else { + successCount.incrementAndGet(); + } + + // Read the current limit + int currentLimit = limit.currentLimit(); + assertThat(currentLimit, is(greaterThanOrEqualTo(config.minLimit()))); + assertThat(currentLimit, is(lessThanOrEqualTo(config.maxLimit()))); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + endLatch.countDown(); + } + }); + } + + startLatch.countDown(); // Start all threads + boolean finished = endLatch.await(10, TimeUnit.SECONDS); + executor.shutdown(); + + assertThat("Test did not complete in time", finished, is(true)); + + assertThat("Total operations mismatch", + threadCount * operationsPerThread, + is(successCount.get() + timeoutCount.get() + dropCount.get())); + } + + @Test + public void testSemaphoreReleased() throws Exception { + Limit limit = AimdLimit.builder() + .minLimit(5) + .initialLimit(5) + .build(); + + for (int i = 0; i < 5000; i++) { + limit.invoke(() -> {}); + } + } + + @Test + public void testSemaphoreReleasedWithToken() { + Limit limit = AimdLimit.builder() + .minLimit(5) + .initialLimit(5) + .build(); + + for (int i = 0; i < 5000; i++) { + Optional token = limit.tryAcquire(); + assertThat(token, not(Optional.empty())); + token.get().success(); + } + } +} diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/ConfiguredLimitTest.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/ConfiguredLimitTest.java new file mode 100644 index 00000000000..9c9a79e96bb --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/ConfiguredLimitTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; + +import io.helidon.config.Config; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ConfiguredLimitTest { + private static Config config; + + @BeforeAll + public static void init() { + config = Config.create(); + } + + @Test + public void testFixed() { + LimitUsingConfig limitConfig = LimitUsingConfig.create(config.get("first")); + Optional configuredLimit = limitConfig.concurrencyLimit(); + assertThat(configuredLimit, not(Optional.empty())); + Limit limit = configuredLimit.get(); + + assertThat(limit.name(), is("server-listener")); + assertThat(limit.type(), is("fixed")); + + FixedLimitConfig prototype = ((FixedLimit) limit).prototype(); + assertThat("Permits", prototype.permits(), is(1)); + assertThat("Queue length", prototype.queueLength(), is(20)); + assertThat("Should be fair", prototype.fair(), is(true)); + assertThat("Queue timeout", prototype.queueTimeout(), is(Duration.ofSeconds(42))); + } + + @Test + public void testAimd() { + LimitUsingConfig limitConfig = LimitUsingConfig.create(config.get("second")); + Optional configuredLimit = limitConfig.concurrencyLimit(); + assertThat(configuredLimit, not(Optional.empty())); + Limit limit = configuredLimit.get(); + + assertThat(limit.name(), is("aimd")); + assertThat(limit.type(), is("aimd")); + + AimdLimitConfig prototype = ((AimdLimit) limit).prototype(); + assertThat("Timeout", prototype.timeout(), is(Duration.ofSeconds(42))); + assertThat("Min limit", prototype.minLimit(), is(11)); + assertThat("Max limit", prototype.maxLimit(), is(22)); + assertThat("Initial limit", prototype.initialLimit(), is(14)); + assertThat("Backoff ratio", prototype.backoffRatio(), is(0.74)); + } +} diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/FixedLimitTest.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/FixedLimitTest.java new file mode 100644 index 00000000000..80315effb50 --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/FixedLimitTest.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +public class FixedLimitTest { + @Test + public void testUnlimited() throws InterruptedException { + FixedLimit limiter = FixedLimit.create(); + int concurrency = 5; + CountDownLatch cdl = new CountDownLatch(1); + CountDownLatch threadsCdl = new CountDownLatch(concurrency); + + Lock lock = new ReentrantLock(); + List result = new ArrayList<>(concurrency); + + Thread[] threads = new Thread[concurrency]; + for (int i = 0; i < concurrency; i++) { + int index = i; + threads[i] = new Thread(() -> { + try { + limiter.invoke(() -> { + threadsCdl.countDown(); + cdl.await(10, TimeUnit.SECONDS); + lock.lock(); + try { + result.add("result_" + index); + } finally { + lock.unlock(); + } + return null; + }); + } catch (Exception e) { + threadsCdl.countDown(); + throw new RuntimeException(e); + } + }); + } + for (Thread thread : threads) { + thread.start(); + } + threadsCdl.await(); + cdl.countDown(); + for (Thread thread : threads) { + thread.join(Duration.ofSeconds(5)); + } + assertThat(result, hasSize(concurrency)); + } + + @Test + public void testLimit() throws Exception { + FixedLimit limiter = FixedLimit.builder() + .permits(1) + .build(); + + int concurrency = 5; + CountDownLatch cdl = new CountDownLatch(1); + CountDownLatch threadsCdl = new CountDownLatch(concurrency); + + Lock lock = new ReentrantLock(); + List result = new ArrayList<>(concurrency); + AtomicInteger failures = new AtomicInteger(); + + Thread[] threads = new Thread[concurrency]; + for (int i = 0; i < concurrency; i++) { + int index = i; + threads[i] = new Thread(() -> { + try { + limiter.invoke(() -> { + threadsCdl.countDown(); + cdl.await(10, TimeUnit.SECONDS); + lock.lock(); + try { + result.add("result_" + index); + } finally { + lock.unlock(); + } + return null; + }); + } catch (LimitException e) { + threadsCdl.countDown(); + failures.incrementAndGet(); + } catch (Exception e) { + threadsCdl.countDown(); + throw new RuntimeException(e); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + // wait for all threads to reach appropriate destination + threadsCdl.await(); + cdl.countDown(); + for (Thread thread : threads) { + thread.join(Duration.ofSeconds(5)); + } + assertThat(failures.get(), is(concurrency - 1)); + assertThat(result.size(), is(1)); + } + + @Test + public void testLimitWithQueue() throws Exception { + FixedLimit limiter = FixedLimit.builder() + .permits(1) + .queueLength(1) + .queueTimeout(Duration.ofSeconds(5)) + .build(); + + int concurrency = 5; + CountDownLatch cdl = new CountDownLatch(1); + + Lock lock = new ReentrantLock(); + List result = new ArrayList<>(concurrency); + AtomicInteger failures = new AtomicInteger(); + + Thread[] threads = new Thread[concurrency]; + for (int i = 0; i < concurrency; i++) { + int index = i; + threads[i] = new Thread(() -> { + try { + limiter.invoke(() -> { + cdl.await(10, TimeUnit.SECONDS); + lock.lock(); + try { + result.add("result_" + index); + } finally { + lock.unlock(); + } + return null; + }); + } catch (LimitException e) { + failures.incrementAndGet(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + // wait for the threads to reach their destination (either failed, or on cdl, or in queue) + TimeUnit.MILLISECONDS.sleep(100); + cdl.countDown(); + for (Thread thread : threads) { + thread.join(Duration.ofSeconds(5)); + } + // 1 submitted, 1 in queue (may be less failures, as the queue length is not guaranteed to be atomic + assertThat(failures.get(), lessThanOrEqualTo(concurrency - 2)); + // may be 2 or more (1 submitted, 1 or more queued) + assertThat(result.size(), greaterThanOrEqualTo(2)); + } + + @Test + public void testSemaphoreReleased() throws Exception { + Limit limit = FixedLimit.builder() + .permits(5) + .build(); + + for (int i = 0; i < 5000; i++) { + limit.invoke(() -> { + }); + } + } + + @Test + public void testSemaphoreReleasedWithQueue() throws Exception { + Limit limit = FixedLimit.builder() + .permits(5) + .queueLength(10) + .queueTimeout(Duration.ofMillis(100)) + .build(); + + for (int i = 0; i < 5000; i++) { + limit.invoke(() -> { + }); + } + } + + @Test + public void testSemaphoreReleasedWithToken() { + Limit limit = FixedLimit.builder() + .permits(5) + .queueLength(10) + .queueTimeout(Duration.ofMillis(100)) + .build(); + + for (int i = 0; i < 5000; i++) { + Optional token = limit.tryAcquire(); + assertThat(token, not(Optional.empty())); + token.get().success(); + } + } +} diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/LimitUsingConfigBlueprint.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/LimitUsingConfigBlueprint.java new file mode 100644 index 00000000000..35d99bb95c2 --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/LimitUsingConfigBlueprint.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.concurrency.limits.spi.LimitProvider; + +@Prototype.Blueprint +@Prototype.Configured +interface LimitUsingConfigBlueprint { + @Option.Provider(value = LimitProvider.class, discoverServices = false) + @Option.Configured + Optional concurrencyLimit(); +} diff --git a/common/concurrency/limits/src/test/resources/application.yaml b/common/concurrency/limits/src/test/resources/application.yaml new file mode 100644 index 00000000000..69540cde1d1 --- /dev/null +++ b/common/concurrency/limits/src/test/resources/application.yaml @@ -0,0 +1,32 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +first: + concurrency-limit: + - type: "fixed" + name: "server-listener" + fair: true + permits: 1 + queue-length: 20 + queue-timeout: "PT42S" +second: + concurrency-limit: + aimd: + timeout: "PT42S" + min-limit: 11 + max-limit: 22 + initial-limit: 14 + backoff-ratio: 0.74 diff --git a/common/concurrency/pom.xml b/common/concurrency/pom.xml new file mode 100644 index 00000000000..1346a2b660a --- /dev/null +++ b/common/concurrency/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + io.helidon.common + helidon-common-project + 4.2.0-SNAPSHOT + ../pom.xml + + + io.helidon.common.concurrency + helidon-common-concurrency-project + Helidon Common Concurrency Project + + pom + + + limits + + diff --git a/common/config/pom.xml b/common/config/pom.xml index 2347a0bdb91..b9238f1f0e9 100644 --- a/common/config/pom.xml +++ b/common/config/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-config Helidon Common Config diff --git a/common/config/src/main/java/io/helidon/common/config/Config.java b/common/config/src/main/java/io/helidon/common/config/Config.java index 5089bcf0368..e536c709b2a 100644 --- a/common/config/src/main/java/io/helidon/common/config/Config.java +++ b/common/config/src/main/java/io/helidon/common/config/Config.java @@ -35,6 +35,17 @@ static Config empty() { return EmptyConfig.EMPTY; } + /** + * Create a new instance of configuration from the default configuration sources. + * In case there is no {@link io.helidon.common.config.spi.ConfigProvider} available, returns + * {@link #empty()}. + * + * @return a new configuration + */ + static Config create() { + return GlobalConfig.create(); + } + /** * Returns the fully-qualified key of the {@code Config} node. *

    @@ -383,8 +394,8 @@ static String escapeName(String name) { * @return unescaped name */ static String unescapeName(String escapedName) { - return escapedName.replaceAll("~1", ".") - .replaceAll("~0", "~"); + return escapedName.replace("~1", ".") + .replace("~0", "~"); } /** diff --git a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java index fd661893a5c..387dfdc2a85 100644 --- a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java +++ b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public final class GlobalConfig { return EMPTY; } // there is a valid provider, let's use its default configuration - return providers.get(0) + return providers.getFirst() .create(); }); private static final AtomicReference CONFIG = new AtomicReference<>(); @@ -100,4 +100,16 @@ public static Config config(Supplier config, boolean overwrite) { } return CONFIG.get(); } + + static Config create() { + List providers = HelidonServiceLoader.create(ServiceLoader.load(ConfigProvider.class)) + .asList(); + // no implementations available, use empty configuration + if (providers.isEmpty()) { + return EMPTY; + } + // there is a valid provider, let's use its default configuration + return providers.getFirst() + .create(); + } } diff --git a/common/configurable/pom.xml b/common/configurable/pom.xml index fb860e83e30..ff539382455 100644 --- a/common/configurable/pom.xml +++ b/common/configurable/pom.xml @@ -24,7 +24,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT Helidon Common Configurable helidon-common-configurable @@ -95,13 +95,13 @@ true - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.codegen - helidon-codegen-apt + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} @@ -130,13 +130,13 @@ - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.codegen - helidon-codegen-apt + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} diff --git a/common/configurable/src/main/java/io/helidon/common/configurable/AllowListConfigBlueprint.java b/common/configurable/src/main/java/io/helidon/common/configurable/AllowListConfigBlueprint.java index 1a3f6ab2bfe..a42923ec308 100644 --- a/common/configurable/src/main/java/io/helidon/common/configurable/AllowListConfigBlueprint.java +++ b/common/configurable/src/main/java/io/helidon/common/configurable/AllowListConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ interface AllowListConfigBlueprint extends Prototype.Factory { /** * Exact strings to deny. * - * @return exact strings to allow + * @return exact strings to deny */ @Option.Configured("deny.exact") @Option.Singular diff --git a/common/configurable/src/main/java/io/helidon/common/configurable/ObserverManager.java b/common/configurable/src/main/java/io/helidon/common/configurable/ObserverManager.java index a76867ba686..2bddadf7880 100644 --- a/common/configurable/src/main/java/io/helidon/common/configurable/ObserverManager.java +++ b/common/configurable/src/main/java/io/helidon/common/configurable/ObserverManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,15 @@ package io.helidon.common.configurable; import java.lang.System.Logger.Level; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -50,7 +53,8 @@ class ObserverManager { private static final LazyValue> OBSERVERS = LazyValue .create(ObserverManager::loadObservers); - private static final Map, SupplierInfo> SUPPLIERS = new ConcurrentHashMap<>(); + private static final Map, SupplierInfo> SUPPLIERS = new HashMap<>(); + private static final ReadWriteLock SUPPLIERS_LOCK = new ReentrantReadWriteLock(); // A given supplier category can have multiple suppliers, so keep track of the next available index by category. private static final Map SUPPLIER_CATEGORY_NEXT_INDEX_VALUES = new ConcurrentHashMap<>(); @@ -71,13 +75,18 @@ private ObserverManager() { static void registerSupplier(Supplier supplier, String supplierCategory, String executorServiceCategory) { - int supplierIndex = SUPPLIER_CATEGORY_NEXT_INDEX_VALUES.computeIfAbsent(supplierCategory, key -> new AtomicInteger()) - .getAndIncrement(); - SUPPLIERS.computeIfAbsent(supplier, - s -> SupplierInfo.create(s, - executorServiceCategory, - supplierCategory, - supplierIndex)); + SUPPLIERS_LOCK.writeLock().lock(); + try { + int supplierIndex = SUPPLIER_CATEGORY_NEXT_INDEX_VALUES.computeIfAbsent(supplierCategory, key -> new AtomicInteger()) + .getAndIncrement(); + SUPPLIERS.computeIfAbsent(supplier, + s -> SupplierInfo.create(s, + executorServiceCategory, + supplierCategory, + supplierIndex)); + } finally { + SUPPLIERS_LOCK.writeLock().unlock(); + } } /** @@ -90,7 +99,13 @@ static void registerSupplier(Supplier supplier, * @throws IllegalStateException if the supplier has not previously registered itself */ static E registerExecutorService(Supplier supplier, E executorService) { - SupplierInfo supplierInfo = SUPPLIERS.get(supplier); + SUPPLIERS_LOCK.readLock().lock(); + SupplierInfo supplierInfo; + try { + supplierInfo = SUPPLIERS.get(supplier); + } finally { + SUPPLIERS_LOCK.readLock().unlock(); + } if (supplierInfo == null) { throw new IllegalStateException("Attempt to register an executor service to an unregistered supplier"); } diff --git a/common/context/pom.xml b/common/context/pom.xml index fdff8e77215..d8ebc45383c 100644 --- a/common/context/pom.xml +++ b/common/context/pom.xml @@ -22,7 +22,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-context Helidon Common Context diff --git a/common/context/src/main/java/io/helidon/common/context/ContextAwareExecutorImpl.java b/common/context/src/main/java/io/helidon/common/context/ContextAwareExecutorImpl.java index 768f89f6a84..1bc8e941197 100644 --- a/common/context/src/main/java/io/helidon/common/context/ContextAwareExecutorImpl.java +++ b/common/context/src/main/java/io/helidon/common/context/ContextAwareExecutorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -123,41 +123,55 @@ protected Collection> wrap(Collection Callable wrap(Callable task) { + Map, Object> properties = new HashMap<>(); + PROVIDERS.forEach(provider -> properties.put(provider.getClass(), provider.data())); Optional context = Contexts.context(); - if (context.isPresent()) { - Map, Object> properties = new HashMap<>(); - PROVIDERS.forEach(provider -> properties.put(provider.getClass(), provider.data())); - return () -> { - try { - PROVIDERS.forEach(provider -> provider.propagateData(properties.get(provider.getClass()))); - return Contexts.runInContext(context.get(), task); - } finally { - PROVIDERS.forEach(provider -> provider.clearData(properties.get(provider.getClass()))); - } - }; - } else { - return task; - } + return context.>map(value -> () -> { + try { + propagateMdcData(properties); + return Contexts.runInContext(value, task); + } finally { + clearMdcData(properties); + } + }).orElseGet(() -> () -> { + try { + propagateMdcData(properties); + return task.call(); + } finally { + clearMdcData(properties); + } + }); } - @SuppressWarnings(value = "unchecked") protected Runnable wrap(Runnable command) { Optional context = Contexts.context(); - if (context.isPresent()) { - Map, Object> properties = new HashMap<>(); - PROVIDERS.forEach(provider -> properties.put(provider.getClass(), provider.data())); - return () -> { - try { - PROVIDERS.forEach(provider -> provider.propagateData(properties.get(provider.getClass()))); - Contexts.runInContext(context.get(), command); - } finally { - PROVIDERS.forEach(provider -> provider.clearData(properties.get(provider.getClass()))); - } - }; - } else { - return command; - } + Map, Object> properties = new HashMap<>(); + PROVIDERS.forEach(provider -> properties.put(provider.getClass(), provider.data())); + return context.map(value -> () -> { + try { + propagateMdcData(properties); + Contexts.runInContext(value, command); + } finally { + clearMdcData(properties); + } + }).orElseGet(() -> () -> { + try { + propagateMdcData(properties); + command.run(); + } finally { + clearMdcData(properties); + } + }); + } + + @SuppressWarnings(value = "unchecked") + private static void propagateMdcData(Map, Object> properties) { + PROVIDERS.forEach(provider -> provider.propagateData(properties.get(provider.getClass()))); + } + + @SuppressWarnings(value = "unchecked") + private static void clearMdcData(Map, Object> properties) { + PROVIDERS.forEach(provider -> provider.clearData(properties.get(provider.getClass()))); } } diff --git a/common/crypto/pom.xml b/common/crypto/pom.xml index 5828acfdb81..9befb1e3286 100644 --- a/common/crypto/pom.xml +++ b/common/crypto/pom.xml @@ -21,7 +21,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/common/features/api/pom.xml b/common/features/api/pom.xml index 8c4a3896753..c1afd0727ed 100644 --- a/common/features/api/pom.xml +++ b/common/features/api/pom.xml @@ -23,8 +23,7 @@ io.helidon.common.features helidon-common-features-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT helidon-common-features-api diff --git a/common/features/features/pom.xml b/common/features/features/pom.xml index c4c9e4183f0..914423afd21 100644 --- a/common/features/features/pom.xml +++ b/common/features/features/pom.xml @@ -23,8 +23,7 @@ io.helidon.common.features helidon-common-features-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT helidon-common-features diff --git a/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java b/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java index 6fd7f937c01..35db736c3a4 100644 --- a/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java +++ b/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,8 @@ static List features(ClassLoader classLoader) { } String module = props.getProperty("m"); if (module == null) { - LOGGER.log(Level.WARNING, "Got module descriptor with no module name. Available properties: " + props); + LOGGER.log(Level.WARNING, "Got module descriptor with no module name. Available properties: " + props + + " at " + url); continue; } FeatureDescriptor.Builder builder = FeatureDescriptor.builder(); diff --git a/common/features/features/src/main/resources/META-INF/native-image/io.helidon.common.features/helidon-common-features/native-image.properties b/common/features/features/src/main/resources/META-INF/native-image/io.helidon.common.features/helidon-common-features/native-image.properties new file mode 100644 index 00000000000..9e70fbb16f9 --- /dev/null +++ b/common/features/features/src/main/resources/META-INF/native-image/io.helidon.common.features/helidon-common-features/native-image.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Needed at build time, as we validate all used features are usable with native-image +Args=--initialize-at-build-time=io.helidon.common.features diff --git a/common/features/pom.xml b/common/features/pom.xml index e657f014e8a..51f6fcf32c6 100644 --- a/common/features/pom.xml +++ b/common/features/pom.xml @@ -23,8 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT io.helidon.common.features diff --git a/common/features/processor/pom.xml b/common/features/processor/pom.xml index d07dd99d820..dedd590ccec 100644 --- a/common/features/processor/pom.xml +++ b/common/features/processor/pom.xml @@ -23,8 +23,7 @@ io.helidon.common.features helidon-common-features-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT helidon-common-features-processor Helidon Common Features Annotation Processor diff --git a/common/key-util/pom.xml b/common/key-util/pom.xml index b2a994bea50..9f7c9ebd634 100644 --- a/common/key-util/pom.xml +++ b/common/key-util/pom.xml @@ -24,7 +24,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-key-util Helidon Common Key Util @@ -81,8 +81,8 @@ - io.helidon.config - helidon-config-metadata-processor + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} @@ -104,8 +104,8 @@ - io.helidon.config - helidon-config-metadata-processor + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} diff --git a/common/mapper/pom.xml b/common/mapper/pom.xml index ea9309098cd..567fb46285c 100644 --- a/common/mapper/pom.xml +++ b/common/mapper/pom.xml @@ -21,7 +21,7 @@ helidon-common-project io.helidon.common - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManagerImpl.java b/common/mapper/src/main/java/io/helidon/common/mapper/MapperManagerImpl.java index a018c446c40..b0e4b2b2752 100644 --- a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManagerImpl.java +++ b/common/mapper/src/main/java/io/helidon/common/mapper/MapperManagerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,14 @@ public Optional> mapper(GenericType Mapper notFoundMapper(GenericType sourceType, GenericType targetType, @@ -99,7 +107,7 @@ private Mapper findMapper(Class sourceT Class targetType, boolean fromTypes, String... qualifiers) { - Mapper mapper = classCache.computeIfAbsent(new ClassCacheKey(sourceType, targetType, qualifiers), key -> { + Mapper mapper = classCache.computeIfAbsent(new ClassCacheKey(sourceType, targetType, List.of(qualifiers)), key -> { // first attempt to find by classes return fromProviders(sourceType, targetType, qualifiers) .orElseGet(() -> { @@ -119,7 +127,7 @@ private Mapper findMapper(GenericType s GenericType targetType, boolean fromClasses, String... qualifiers) { - Mapper mapper = typeCache.computeIfAbsent(new GenericCacheKey(sourceType, targetType, qualifiers), key -> { + Mapper mapper = typeCache.computeIfAbsent(new GenericCacheKey(sourceType, targetType, List.of(qualifiers)), key -> { // first attempt to find by types return fromProviders(sourceType, targetType, qualifiers) .orElseGet(() -> { @@ -199,10 +207,10 @@ private Mapper findMapper(GenericType s qualifiers); } - private record GenericCacheKey(GenericType sourceType, GenericType targetType, String... qualifiers) { + private record GenericCacheKey(GenericType sourceType, GenericType targetType, List qualifiers) { } - private record ClassCacheKey(Class sourceType, Class targetType, String... qualifiers) { + private record ClassCacheKey(Class sourceType, Class targetType, List qualifiers) { } @SuppressWarnings("rawtypes") diff --git a/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTest.java b/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTest.java index c8ad7046079..06bfe86b075 100644 --- a/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTest.java +++ b/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -243,4 +243,34 @@ void testEmptyValue() { assertThrows(MapperException.class, () -> doubleValue.as(Integer.class)); } + + @Test + void testCacheWorks() { + MapperManagerImpl mm = new MapperManagerImpl(MapperManager.builder() + .addMapperProvider(new TestProvider())); + assertThat(mm.classCacheSize(), is(0)); + assertThat(mm.typeCacheSize(), is(0)); + + mm.map("value", String.class, String.class, "value"); + mm.map("value", String.class, String.class, "value"); + mm.map("value", String.class, String.class, "value"); + assertThat(mm.classCacheSize(), is(1)); + + mm.map("value", GenericType.STRING, GenericType.STRING, "value"); + mm.map("value", GenericType.STRING, GenericType.STRING, "value"); + mm.map("value", GenericType.STRING, GenericType.STRING, "value"); + assertThat(mm.typeCacheSize(), is(1)); + } + + private static class TestProvider implements MapperProvider { + @Override + public ProviderResponse mapper(Class sourceClass, Class targetClass, String qualifier) { + return new ProviderResponse(Support.SUPPORTED, req -> req); + } + + @Override + public ProviderResponse mapper(GenericType sourceType, GenericType targetType, String qualifier) { + return MapperProvider.super.mapper(sourceType, targetType, qualifier); + } + } } \ No newline at end of file diff --git a/common/media-type/pom.xml b/common/media-type/pom.xml index da1b6ccfc4c..66af20b4b36 100644 --- a/common/media-type/pom.xml +++ b/common/media-type/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-media-type diff --git a/common/parameters/pom.xml b/common/parameters/pom.xml index 18f9f5fc0d0..a40287075e7 100644 --- a/common/parameters/pom.xml +++ b/common/parameters/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-parameters Helidon Common Parameters diff --git a/common/pom.xml b/common/pom.xml index 898d6543c98..b62636185ab 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.common helidon-common-project @@ -55,6 +55,7 @@ tls types uri + concurrency diff --git a/common/processor/class-model/pom.xml b/common/processor/class-model/pom.xml index 283594c5b4e..d8b8a605312 100644 --- a/common/processor/class-model/pom.xml +++ b/common/processor/class-model/pom.xml @@ -22,7 +22,7 @@ io.helidon.common.processor helidon-common-processor-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-processor-class-model diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java index 5a08fac818d..494ad5f7007 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java @@ -179,6 +179,17 @@ public List typeParameters() { return List.of(); } + @Override + public List lowerBounds() { + // not yet supported + return List.of(); + } + + @Override + public List upperBounds() { + return List.of(bound.genericTypeName()); + } + @Override public String toString() { if (bound == null) { diff --git a/common/processor/helidon-copyright/pom.xml b/common/processor/helidon-copyright/pom.xml index e4a4b932ac9..c8295af88f1 100644 --- a/common/processor/helidon-copyright/pom.xml +++ b/common/processor/helidon-copyright/pom.xml @@ -23,8 +23,7 @@ io.helidon.common.processor helidon-common-processor-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT helidon-common-processor-helidon-copyright diff --git a/common/processor/pom.xml b/common/processor/pom.xml index bd781a684ef..481b7feb27f 100644 --- a/common/processor/pom.xml +++ b/common/processor/pom.xml @@ -23,8 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT pom diff --git a/common/processor/processor/pom.xml b/common/processor/processor/pom.xml index e9e4ce53f2a..85b9a054265 100644 --- a/common/processor/processor/pom.xml +++ b/common/processor/processor/pom.xml @@ -23,8 +23,7 @@ io.helidon.common.processor helidon-common-processor-project - 4.1.0-SNAPSHOT - ../pom.xml + 4.2.0-SNAPSHOT helidon-common-processor diff --git a/common/reactive/pom.xml b/common/reactive/pom.xml index e97add8a485..1ee0d9e3ea1 100644 --- a/common/reactive/pom.xml +++ b/common/reactive/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-reactive Helidon Common Reactive diff --git a/common/security/pom.xml b/common/security/pom.xml index fe5853f43db..5884db66599 100644 --- a/common/security/pom.xml +++ b/common/security/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-security Helidon Common Security diff --git a/common/socket/pom.xml b/common/socket/pom.xml index fd372809442..71c9d06ea27 100644 --- a/common/socket/pom.xml +++ b/common/socket/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-socket Helidon Common Socket @@ -71,8 +71,8 @@ true - io.helidon.config - helidon-config-metadata-processor + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} @@ -94,8 +94,8 @@ - io.helidon.config - helidon-config-metadata-processor + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} diff --git a/common/socket/src/main/java/io/helidon/common/socket/SmartSocketWriter.java b/common/socket/src/main/java/io/helidon/common/socket/SmartSocketWriter.java new file mode 100644 index 00000000000..1d310535b4b --- /dev/null +++ b/common/socket/src/main/java/io/helidon/common/socket/SmartSocketWriter.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.socket; + +import java.util.concurrent.ExecutorService; + +import io.helidon.common.buffers.BufferData; + +/** + * A special socket write that starts async but may switch to sync mode if it + * detects that the async queue size is below {@link #QUEUE_SIZE_THRESHOLD}. + * If it switches to sync mode, it shall never return back to async mode. + */ +public class SmartSocketWriter extends SocketWriter { + private static final long WINDOW_SIZE = 1000; + private static final double QUEUE_SIZE_THRESHOLD = 2.0; + + private final SocketWriterAsync asyncWriter; + private volatile long windowIndex; + private volatile boolean asyncMode; + + SmartSocketWriter(ExecutorService executor, HelidonSocket socket, int writeQueueLength) { + super(socket); + this.asyncWriter = new SocketWriterAsync(executor, socket, writeQueueLength); + this.asyncMode = true; + this.windowIndex = 0L; + } + + @Override + public void write(BufferData... buffers) { + for (BufferData buffer : buffers) { + write(buffer); + } + } + + @Override + public void write(BufferData buffer) { + if (asyncMode) { + asyncWriter.write(buffer); + if (++windowIndex % WINDOW_SIZE == 0 && asyncWriter.avgQueueSize() < QUEUE_SIZE_THRESHOLD) { + asyncMode = false; + } + } else { + asyncWriter.drainQueue(); + writeNow(buffer); // blocking write + } + } +} diff --git a/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java b/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java index 7d2480c02e3..f5323ab6360 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java +++ b/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,15 +44,19 @@ protected SocketWriter(HelidonSocket socket) { * @param socket socket to write to * @param writeQueueLength maximal number of queued writes, write operation will block if the queue is full; if set to * {code 1} or lower, write queue is disabled and writes are direct to socket (blocking) + * @param smartAsyncWrites flag to enable smart async writes, see {@link io.helidon.common.socket.SmartSocketWriter} * @return a new socket writer */ public static SocketWriter create(ExecutorService executor, HelidonSocket socket, - int writeQueueLength) { + int writeQueueLength, + boolean smartAsyncWrites) { if (writeQueueLength <= 1) { return new SocketWriterDirect(socket); } else { - return new SocketWriterAsync(executor, socket, writeQueueLength); + return smartAsyncWrites + ? new SmartSocketWriter(executor, socket, writeQueueLength) + : new SocketWriterAsync(executor, socket, writeQueueLength); } } diff --git a/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java b/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java index 764e273170a..a663191f414 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java +++ b/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ class SocketWriterAsync extends SocketWriter implements DataWriter { private static final System.Logger LOGGER = System.getLogger(SocketWriterAsync.class.getName()); private static final BufferData CLOSING_TOKEN = BufferData.empty(); + private final ExecutorService executor; private final ArrayBlockingQueue writeQueue; private final CountDownLatch cdl = new CountDownLatch(1); @@ -40,6 +41,7 @@ class SocketWriterAsync extends SocketWriter implements DataWriter { private volatile Throwable caught; private volatile boolean run = true; private Thread thread; + private double avgQueueSize; /** * A new socket writer. @@ -116,7 +118,8 @@ private void run() { CompositeBufferData toWrite = BufferData.createComposite(writeQueue.take()); // wait if the queue is empty // we only want to read a certain amount of data, if somebody writes huge amounts // we could spin here forever and run out of memory - for (int i = 0; i < 1000; i++) { + int queueSize = 1; + for (; queueSize <= 1000; queueSize++) { BufferData newBuf = writeQueue.poll(); // drain ~all elements from the queue, don't wait. if (newBuf == null) { break; @@ -124,6 +127,7 @@ private void run() { toWrite.add(newBuf); } writeNow(toWrite); + avgQueueSize = (avgQueueSize + queueSize) / 2.0; } cdl.countDown(); } catch (Throwable e) { @@ -141,4 +145,15 @@ private void checkRunning() { throw new SocketWriterException(caught); } } + + void drainQueue() { + BufferData buffer; + while ((buffer = writeQueue.poll()) != null) { + writeNow(buffer); + } + } + + double avgQueueSize() { + return avgQueueSize; + } } diff --git a/common/task/pom.xml b/common/task/pom.xml index d6d7d195029..0a7e3989b1c 100644 --- a/common/task/pom.xml +++ b/common/task/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-task Helidon Common Task diff --git a/common/testing/http-junit5/pom.xml b/common/testing/http-junit5/pom.xml index 0eab7a8baee..4289b78ebe9 100644 --- a/common/testing/http-junit5/pom.xml +++ b/common/testing/http-junit5/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.testing helidon-common-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-testing-http-junit5 diff --git a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java index 49eb2b41651..a75c30f41c3 100644 --- a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java +++ b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,7 +217,7 @@ protected boolean matchesSafely(Headers httpHeaders) { if (httpHeaders.contains(name)) { Header headerValue = httpHeaders.get(name); if (headerValue.allValues().size() == 1) { - return valuesMatcher.matches(headerValue.value()); + return valuesMatcher.matches(headerValue.get()); } return false; } diff --git a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java index 92ec6aed91a..75838cd1710 100644 --- a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java +++ b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java @@ -605,6 +605,15 @@ public SocketHttpClient sendChunk(String payload) throws IOException { return this; } + /** + * Provides access to underlying socket reader. + * + * @return the reader + */ + public BufferedReader socketReader() { + return socketReader; + } + /** * Override this to send a specific payload. * diff --git a/common/testing/junit5/pom.xml b/common/testing/junit5/pom.xml index eb75fad7f76..7357ab60f38 100644 --- a/common/testing/junit5/pom.xml +++ b/common/testing/junit5/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.testing helidon-common-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-testing-junit5 diff --git a/common/testing/pom.xml b/common/testing/pom.xml index eba5d08485d..2341db93845 100644 --- a/common/testing/pom.xml +++ b/common/testing/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.common.testing diff --git a/common/tls/pom.xml b/common/tls/pom.xml index 783d81f50b7..6132cc27901 100644 --- a/common/tls/pom.xml +++ b/common/tls/pom.xml @@ -24,7 +24,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-tls @@ -81,13 +81,13 @@ - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.codegen - helidon-codegen-apt + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} @@ -104,13 +104,13 @@ - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.codegen - helidon-codegen-apt + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} diff --git a/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java b/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java index 058d1b37425..65a4c50a871 100644 --- a/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java +++ b/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java @@ -240,7 +240,7 @@ static List createTrust(Keys config) { /** * Enabled cipher suites for TLS communication. * - * @return cipher suits to enable, by default (or if list is empty), all available cipher suites + * @return cipher suites to enable, by default (or if list is empty), all available cipher suites * are enabled */ @Option.Configured("cipher-suite") diff --git a/common/types/README.md b/common/types/README.md index 1fb2e39a0a4..b83dbbed607 100644 --- a/common/types/README.md +++ b/common/types/README.md @@ -5,4 +5,94 @@ Language types abstraction used during annotation processing (and instead of ref As types are required for annotation processors, they cannot be generated using annotation processors for builder. To work around this cyclic dependency problem, there is a module `builder/tests/common-types` that contains the correct blueprints and static methods to generate the code required for this module. -If a change is needed, generate the code using that module, and copy all the types (blueprint, static methods, and generated classes) here. \ No newline at end of file +If a change is needed, generate the code using that module, and copy all the types (blueprint, static methods, and generated classes) here. + + +# TypeName + +TypeName represents a type (class, interface, record). +Its `equals` and `hashCode` methods ignore generics (i.e. `Supplier` and `Supplier` are equal and have +the same hashCode - type erasure like behavior). + +If there is a requirement to compare based on generic declaration, use `ResolvedType`. + +## Handling of generics + +Depending on how a type name is created, the generic information may be available: + +1. `TypeName.create(SomeType.class)` - contains "raw" information - package name, class name +2. `TypeName.create(Type)` - when created from a `io.helidon.common.GenericType`, or `java.lang.reflect.ParameterizedType`, the type will contain type arguments (i.e. for `GenericType>` there will be a type `List` with type argument `String`) +3. Through codegen factories (annotation processing, classpath scanning, reflection) - see below + +TypeName is created for: + +1. Type declaration (`class MyClass...` - regardless of generics) - raw type name, accessible through `TypeInfo.rawType()`, or `TypeInfo.typeName()` if the type info was created for a raw type +2. Type declaration (`class MyClass`) - with all declared type parameters, accessible through `TypeInfo.declaredType()` +3. A type usage (`implements Supplier`) for the example above - with all type parameter information, accessible through `TypeInfo.typeName()` on type info of superclass or implemented interface +4. Wildcard usage (`List`) in parameter arguments + +Raw type: +```yaml +package: "com.example" +class-name: "MyClass" +``` + +Declared type (`MyClass`) : +```yaml +package: "com.example" +class-name: "MyClass" +type-parameters: # list of type names + - class-name: "X" + generic: true + upper-bounds: # list of type names - if not present, `Object` is expected (for ? extends X) + - class-name: "CharSequence" + - class-name: "Serializable" + lower-bounds: # list of type names - if not present, no lower bounds (for ? super X) + +``` +Type usage (`implements Supplier`): +```yaml +package: "java.util.function" +class-name: "Supplier" +type-parameters: # list of type names + - class-name: "X" + generic: true + upper-bounds: + - class-name: "CharSequence" + - class-name: "Serializable" +``` + +Type usage (`implements Supplier`): +```yaml +package: "java.util.function" +class-name: "Supplier" +type-parameters: # list of type names + - class-name: "CharSequence" +``` + +Wildcard usage (`List`): +```yaml +package: "java.util" +class-name: "List" +type-parameters: # list of type names + - class-name: "CharSequence" + package-name: "java.lang" + generic: true + wildcard: true + upper-bounds: + - class-name: "CharSequence" +``` + + +Wildcard usage (`List`): +```yaml +package: "java.util" +class-name: "List" +type-parameters: # list of type names + - class-name: "?" + generic: true + wildcard: true + lower-bounds: + - class-name: "String" +``` + diff --git a/common/types/pom.xml b/common/types/pom.xml index 25b8cd1ddbb..4a1adc9a36c 100644 --- a/common/types/pom.xml +++ b/common/types/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-types diff --git a/common/types/src/main/java/io/helidon/common/types/Annotated.java b/common/types/src/main/java/io/helidon/common/types/Annotated.java index 07f13ac5c29..3aab5620bdf 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotated.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotated.java @@ -43,6 +43,8 @@ public interface Annotated { *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @return list of all meta annotations of this element */ @@ -85,11 +87,10 @@ default Optional findAnnotation(TypeName annotationType) { * @see #findAnnotation(TypeName) */ default Annotation annotation(TypeName annotationType) { - return findAnnotation(annotationType).orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " " - + "is not present. Guard " - + "with hasAnnotation(), or " - + "use findAnnotation() " - + "instead")); + return findAnnotation(annotationType) + .orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " is not present. " + + "Guard with hasAnnotation(), " + + "or use findAnnotation() instead")); } /** diff --git a/common/types/src/main/java/io/helidon/common/types/Annotation.java b/common/types/src/main/java/io/helidon/common/types/Annotation.java index 5e829998047..bd27d407599 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotation.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ package io.helidon.common.types; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -153,7 +155,9 @@ static Annotation create(TypeName annoTypeName, Map values) { */ abstract class BuilderBase, PROTOTYPE extends Annotation> implements Prototype.Builder { + private final List metaAnnotations = new ArrayList<>(); private final Map values = new LinkedHashMap<>(); + private boolean isMetaAnnotationsMutated; private TypeName typeName; /** @@ -163,7 +167,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -171,6 +175,10 @@ protected BuilderBase() { public BUILDER from(Annotation prototype) { typeName(prototype.typeName()); addValues(prototype.values()); + if (!isMetaAnnotationsMutated) { + metaAnnotations.clear(); + } + addMetaAnnotations(prototype.metaAnnotations()); return self(); } @@ -182,7 +190,15 @@ public BUILDER from(Annotation prototype) { */ public BUILDER from(Annotation.BuilderBase builder) { builder.typeName().ifPresent(this::typeName); - addValues(builder.values()); + addValues(builder.values); + if (isMetaAnnotationsMutated) { + if (builder.isMetaAnnotationsMutated) { + addMetaAnnotations(builder.metaAnnotations); + } + } else { + metaAnnotations.clear(); + addMetaAnnotations(builder.metaAnnotations); + } return self(); } @@ -293,6 +309,64 @@ public BUILDER putValue(String key, Object value) { return self(); } + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotations list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER metaAnnotations(List metaAnnotations) { + Objects.requireNonNull(metaAnnotations); + isMetaAnnotationsMutated = true; + this.metaAnnotations.clear(); + this.metaAnnotations.addAll(metaAnnotations); + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotations list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotations(List metaAnnotations) { + Objects.requireNonNull(metaAnnotations); + isMetaAnnotationsMutated = true; + this.metaAnnotations.addAll(metaAnnotations); + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotation list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotation(Annotation metaAnnotation) { + Objects.requireNonNull(metaAnnotation); + this.metaAnnotations.add(metaAnnotation); + isMetaAnnotationsMutated = true; + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param consumer list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotation(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = Annotation.builder(); + consumer.accept(builder); + this.metaAnnotations.add(builder.build()); + return self(); + } + /** * The type name, e.g., {@link java.util.Objects} -> "java.util.Objects". * @@ -311,6 +385,15 @@ public Map values() { return values; } + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return the meta annotations + */ + public List metaAnnotations() { + return metaAnnotations; + } + @Override public String toString() { return "AnnotationBuilder{" @@ -341,6 +424,7 @@ protected void validatePrototype() { */ protected static class AnnotationImpl implements Annotation { + private final List metaAnnotations; private final Map values; private final TypeName typeName; @@ -352,6 +436,7 @@ protected static class AnnotationImpl implements Annotation { protected AnnotationImpl(Annotation.BuilderBase builder) { this.typeName = builder.typeName().get(); this.values = Collections.unmodifiableMap(new LinkedHashMap<>(builder.values())); + this.metaAnnotations = List.copyOf(builder.metaAnnotations()); } @Override @@ -369,6 +454,11 @@ public Map values() { return values; } + @Override + public List metaAnnotations() { + return metaAnnotations; + } + @Override public String toString() { return "Annotation{" @@ -386,7 +476,7 @@ public boolean equals(Object o) { return false; } return Objects.equals(typeName, other.typeName()) - && Objects.equals(values, other.values()); + && Objects.equals(values, other.values()); } @Override diff --git a/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java b/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java index 13e2a99d5f6..b51fb659b6d 100644 --- a/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,6 +79,15 @@ interface AnnotationBlueprint { @Option.Singular Map values(); + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return list of all annotations declared on the annotation type, or inherited from them + */ + @Option.Redundant + @Option.Singular + List metaAnnotations(); + /** * The value property. * @@ -653,4 +662,19 @@ default > Optional> enumValues(String property, Class< return AnnotationSupport.asEnums(typeName(), values(), property, type); } + /** + * Check if {@link io.helidon.common.types.Annotation#metaAnnotations()} contains an annotation of the provided type. + *

    + * Note: we ignore {@link java.lang.annotation.Target}, {@link java.lang.annotation.Inherited}, + * {@link java.lang.annotation.Documented}, and {@link java.lang.annotation.Retention}. + * + * @param annotationType type of annotation + * @return {@code true} if the annotation is declared on this annotation, or is inherited from a declared annotation + */ + default boolean hasMetaAnnotation(TypeName annotationType) { + return metaAnnotations() + .stream() + .map(Annotation::typeName) + .anyMatch(annotationType::equals); + } } diff --git a/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java b/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java index 0101b393de7..b21a8a1a917 100644 --- a/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -532,6 +532,14 @@ private static String asString(TypeName typeName, String property, Object value) return str; } + if (value instanceof TypeName tn) { + return tn.fqName(); + } + + if (value instanceof EnumValue ev) { + return ev.name(); + } + if (value instanceof List) { throw new IllegalArgumentException(typeName.fqName() + " property " + property + " is a list, cannot be converted to String"); @@ -619,22 +627,30 @@ private static Class asClass(TypeName typeName, String property, Object value if (value instanceof Class theClass) { return theClass; } - if (value instanceof String str) { - try { - return Class.forName(str); - } catch (ClassNotFoundException e) { + + String className = switch (value) { + case TypeName tn -> tn.name(); + case String str -> str; + default -> { throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type String and value \"" + str + "\"" + + " of type " + value.getClass().getName() + " cannot be converted to Class"); } - } + }; - throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type " + value.getClass().getName() - + " cannot be converted to Class"); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(typeName.fqName() + " property " + property + + " of type String and value \"" + className + "\"" + + " cannot be converted to Class"); + } } private static TypeName asTypeName(TypeName typeName, String property, Object value) { + if (value instanceof TypeName tn) { + return tn; + } if (value instanceof Class theClass) { return TypeName.create(theClass); } @@ -664,6 +680,15 @@ private static > T asEnum(TypeName typeName, String property, if (value instanceof String str) { return Enum.valueOf(type, str); } + if (value instanceof EnumValue enumValue) { + if (enumValue.type().equals(TypeName.create(type))) { + return Enum.valueOf(type, enumValue.name()); + } + + throw new IllegalStateException("Property " + property + " is of enum type for enum " + + enumValue.type().fqName() + ", yet you requested " + + type.getName()); + } throw new IllegalArgumentException(typeName.fqName() + " property " + property + " of type " + value.getClass().getName() diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignature.java b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java new file mode 100644 index 00000000000..a167313516d --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; + +/** + * Signature of a {@link io.helidon.common.types.TypedElementInfo}. + *

    + * The {@link io.helidon.common.types.TypedElementInfo#signature()} is intended to compare + * fields, methods, and constructors across type hierarchy - for example when looking for a method + * that we override. + *

    + * The following information is used for equals and hash-code: + *

      + *
    • Field: field name
    • + *
    • Constructor: parameter types
    • + *
    • Method: method name, parameter types
    • + *
    • Parameter: this signature is not useful, as we cannot depend on parameter names
    • + *
    + * + * The signature has well-defined {@code hashCode} and {@code equals} methods, + * so it can be safely used as a key in a {@link java.util.Map}. + *

    + * This interface is sealed, an instance can only be obtained + * from {@link io.helidon.common.types.TypedElementInfo#signature()}. + * + * @see #text() + */ +public sealed interface ElementSignature permits ElementSignatures.FieldSignature, + ElementSignatures.MethodSignature, + ElementSignatures.ParameterSignature, + ElementSignatures.NoSignature { + /** + * Type of the element. Resolves as follows: + *

      + *
    • Field: type of the field
    • + *
    • Constructor: void
    • + *
    • Method: method return type
    • + *
    • Parameter: parameter type
    • + *
    + * + * @return type of this element, never used for equals or hashCode + */ + TypeName type(); + + /** + * Name of the element. For constructor, this always returns {@code }, + * for parameters, this method may return the real parameter name or an index + * parameter name depending on the source of the information (during annotation processing, + * this would be the actual parameter name, when classpath scanning, this would be something like + * {@code param0}. + * + * @return name of this element + */ + String name(); + + /** + * Types of parameters if this represents a method or a constructor, + * empty {@link java.util.List} otherwise. + * + * @return parameter types + */ + List parameterTypes(); + + /** + * A text representation of this signature. + * + *
      + *
    • Field: field name (such as {@code myNiceField}
    • + *
    • Constructor: comma separated parameter types (no generics) in parentheses (such as + * {@code (java.lang.String,java.util.List)})
    • + *
    • Method: method name, parameter types (no generics) in parentheses (such as + * {@code methodName(java.lang.String,java.util.List)}
    • + *
    • Parameter: parameter name (such as {@code myParameter} or {@code param0} - not very useful, as parameter names + * are not carried over to compiled code in Java
    • + *
    + * + * @return text representation + */ + String text(); +} diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java b/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java new file mode 100644 index 00000000000..4f5be563f29 --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class ElementSignatures { + private ElementSignatures() { + } + + static ElementSignature createNone() { + return new NoSignature(); + } + + static ElementSignature createField(TypeName type, + String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new FieldSignature(type, name); + } + + static ElementSignature createConstructor(List parameters) { + Objects.requireNonNull(parameters); + return new MethodSignature(TypeNames.PRIMITIVE_VOID, + "", + parameters); + } + + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + Objects.requireNonNull(returnType); + Objects.requireNonNull(name); + Objects.requireNonNull(parameters); + return new MethodSignature(returnType, + name, + parameters); + } + + static ElementSignature createParameter(TypeName type, String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new ParameterSignature(type, name); + } + + static final class FieldSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private FieldSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldSignature that)) { + return false; + } + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class MethodSignature implements ElementSignature { + private final TypeName type; + private final String name; + private final List parameters; + private final String text; + private final boolean constructor; + + private MethodSignature(TypeName type, + String name, + List parameters) { + this.type = type; + this.name = name; + this.parameters = parameters; + if (name.equals("")) { + this.constructor = true; + this.text = parameterTypesSection(parameters, ",", TypeName::fqName); + } else { + this.constructor = false; + this.text = name + parameterTypesSection(parameters, ",", TypeName::fqName); + } + + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return parameters; + } + + @Override + public String text() { + return text; + } + + @Override + public String toString() { + if (constructor) { + return text; + } else { + return type.resolvedName() + " " + name + parameterTypesSection(parameters, + ", ", + TypeName::resolvedName); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodSignature that)) { + return false; + } + return Objects.equals(name, that.name) && Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters); + } + } + + static final class ParameterSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private ParameterSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParameterSignature that)) { + return false; + } + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class NoSignature implements ElementSignature { + @Override + public TypeName type() { + return TypeNames.PRIMITIVE_VOID; + } + + @Override + public String name() { + return ""; + } + + @Override + public String text() { + return ""; + } + + @Override + public String toString() { + return text(); + } + + @Override + public List parameterTypes() { + return List.of(); + } + } + + private static String parameterTypesSection(List parameters, + String delimiter, + Function typeMapper) { + return parameters.stream() + .map(typeMapper) + .collect(Collectors.joining(delimiter, "(", ")")); + } +} diff --git a/common/types/src/main/java/io/helidon/common/types/EnumValue.java b/common/types/src/main/java/io/helidon/common/types/EnumValue.java new file mode 100644 index 00000000000..3a405d354f2 --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/EnumValue.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Objects; + +/** + * When creating an {@link io.helidon.common.types.Annotation}, we may need to create an enum value + * without access to the enumeration. + *

    + * In such a case, you can use this type when calling {@link io.helidon.common.types.Annotation.Builder#putValue(String, Object)} + */ +public interface EnumValue { + /** + * Create a new enum value, when the enum is not available on classpath. + * + * @param enumType type of the enumeration + * @param enumName value of the enumeration + * @return enum value + */ + static EnumValue create(TypeName enumType, String enumName) { + Objects.requireNonNull(enumType); + Objects.requireNonNull(enumName); + return new EnumValueImpl(enumType, enumName); + } + + /** + * Create a new enum value. + * + * @param type enum type + * @param value enum value constant + * @return new enum value + * @param type of the enum + */ + static > EnumValue create(Class type, T value) { + Objects.requireNonNull(type); + Objects.requireNonNull(value); + + return new EnumValueImpl(TypeName.create(type), value.name()); + } + + /** + * Type of the enumeration. + * + * @return type name of the enumeration + */ + TypeName type(); + + /** + * The enum value. + * + * @return enum value + */ + String name(); +} diff --git a/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java new file mode 100644 index 00000000000..f2205b2452e --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Objects; + +final class EnumValueImpl implements EnumValue { + private final TypeName type; + private final String name; + + EnumValueImpl(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EnumValue enumValue)) { + return false; + } + return Objects.equals(type, enumValue.type()) && Objects.equals(name, enumValue.name()); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + + @Override + public String toString() { + return type.fqName() + "." + name; + } +} diff --git a/common/types/src/main/java/io/helidon/common/types/Modifier.java b/common/types/src/main/java/io/helidon/common/types/Modifier.java index 87a1e75e4db..7d36ffc7ece 100644 --- a/common/types/src/main/java/io/helidon/common/types/Modifier.java +++ b/common/types/src/main/java/io/helidon/common/types/Modifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,23 @@ public enum Modifier { /** * The {@code final} modifier. */ - FINAL("final"); + FINAL("final"), + /** + * The {@code transient} modifier. + */ + TRANSIENT("transient"), + /** + * The {@code volatile} modifier. + */ + VOLATILE("volatile"), + /** + * The {@code synchronized} modifier. + */ + SYNCHRONIZED("synchronized"), + /** + * The {@code native} modifier. + */ + NATIVE("native"); private final String modifierName; diff --git a/common/types/src/main/java/io/helidon/common/types/ResolvedType.java b/common/types/src/main/java/io/helidon/common/types/ResolvedType.java new file mode 100644 index 00000000000..f78594ff1f0 --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ResolvedType.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.lang.reflect.Type; + +/** + * A wrapper for {@link io.helidon.common.types.TypeName} that uses the resolved name for equals and hashCode. + * This allows us to collect interfaces including type arguments. + * + * @see TypeName#resolvedName() + */ +public interface ResolvedType { + /** + * Create a type name from a type (such as class). + * + * @param type the type + * @return type name for the provided type + */ + static ResolvedType create(Type type) { + return new ResolvedTypeImpl(TypeName.create(type)); + } + + /** + * Creates a type name from a fully qualified class name. + * + * @param typeName the FQN of the class type + * @return the TypeName for the provided type name + */ + static ResolvedType create(String typeName) { + return new ResolvedTypeImpl(TypeName.create(typeName)); + } + + /** + * Create a type name from a type name. + * + * @param typeName the type + * @return type name for the provided type + */ + static ResolvedType create(TypeName typeName) { + if (typeName instanceof ResolvedType rt) { + return rt; + } + return new ResolvedTypeImpl(typeName); + } + + /** + * Provides the underlying type name that backs this resolved type. + * + * @return the type name this resolved type represents + */ + TypeName type(); + + /** + * The resolved name including all type arguments. + * + * @return fully qualified class name with all type arguments + */ + String resolvedName(); +} diff --git a/common/types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java b/common/types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java new file mode 100644 index 00000000000..eddfb6a7d0d --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +class ResolvedTypeImpl implements ResolvedType, Comparable { + private final TypeName typeName; + private final String resolvedName; + private final boolean noTypes; + + ResolvedTypeImpl(TypeName typeName) { + this.typeName = typeName; + this.resolvedName = typeName.resolvedName(); + this.noTypes = typeName.typeArguments().isEmpty(); + } + + @Override + public TypeName type() { + return typeName; + } + + @Override + public String resolvedName() { + return resolvedName; + } + + @Override + public int hashCode() { + return noTypes ? typeName.hashCode() : resolvedName.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ResolvedType other)) { + return false; + } + if (other instanceof ResolvedTypeImpl rti) { + return resolvedName.equals(rti.resolvedName); + } + return other.type().resolvedName().equals(resolvedName); + } + + @Override + public int compareTo(ResolvedType o) { + int diff = resolvedName.compareTo(o.type().resolvedName()); + if (diff != 0) { + // different name + return diff; + } + diff = Boolean.compare(typeName.primitive(), o.type().primitive()); + if (diff != 0) { + return diff; + } + return Boolean.compare(typeName.array(), o.type().array()); + } + + @Override + public String toString() { + return resolvedName; + } +} diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java index d47b9f04df1..eff40c41b6b 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java @@ -75,12 +75,19 @@ abstract class BuilderBase elementModifiers = new LinkedHashSet<>(); private final Set modifiers = new LinkedHashSet<>(); private AccessModifier accessModifier; + private boolean isAnnotationsMutated; + private boolean isElementInfoMutated; + private boolean isInheritedAnnotationsMutated; + private boolean isInterfaceTypeInfoMutated; + private boolean isOtherElementInfoMutated; private ElementKind kind; private Object originatingElement; private String description; private String module; private String typeKind; private TypeInfo superTypeInfo; + private TypeName declaredType; + private TypeName rawType; private TypeName typeName; /** @@ -90,28 +97,45 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance */ public BUILDER from(TypeInfo prototype) { typeName(prototype.typeName()); + rawType(prototype.rawType()); + declaredType(prototype.declaredType()); description(prototype.description()); typeKind(prototype.typeKind()); kind(prototype.kind()); + if (!isElementInfoMutated) { + elementInfo.clear(); + } addElementInfo(prototype.elementInfo()); + if (!isOtherElementInfoMutated) { + otherElementInfo.clear(); + } addOtherElementInfo(prototype.otherElementInfo()); addReferencedTypeNamesToAnnotations(prototype.referencedTypeNamesToAnnotations()); addReferencedModuleNames(prototype.referencedModuleNames()); superTypeInfo(prototype.superTypeInfo()); + if (!isInterfaceTypeInfoMutated) { + interfaceTypeInfo.clear(); + } addInterfaceTypeInfo(prototype.interfaceTypeInfo()); addModifiers(prototype.modifiers()); addElementModifiers(prototype.elementModifiers()); accessModifier(prototype.accessModifier()); module(prototype.module()); originatingElement(prototype.originatingElement()); + if (!isAnnotationsMutated) { + annotations.clear(); + } addAnnotations(prototype.annotations()); + if (!isInheritedAnnotationsMutated) { + inheritedAnnotations.clear(); + } addInheritedAnnotations(prototype.inheritedAnnotations()); return self(); } @@ -124,27 +148,67 @@ public BUILDER from(TypeInfo prototype) { */ public BUILDER from(TypeInfo.BuilderBase builder) { builder.typeName().ifPresent(this::typeName); + builder.rawType().ifPresent(this::rawType); + builder.declaredType().ifPresent(this::declaredType); builder.description().ifPresent(this::description); builder.typeKind().ifPresent(this::typeKind); builder.kind().ifPresent(this::kind); - addElementInfo(builder.elementInfo()); - addOtherElementInfo(builder.otherElementInfo()); - addReferencedTypeNamesToAnnotations(builder.referencedTypeNamesToAnnotations()); - addReferencedModuleNames(builder.referencedModuleNames()); + if (isElementInfoMutated) { + if (builder.isElementInfoMutated) { + addElementInfo(builder.elementInfo); + } + } else { + elementInfo.clear(); + addElementInfo(builder.elementInfo); + } + if (isOtherElementInfoMutated) { + if (builder.isOtherElementInfoMutated) { + addOtherElementInfo(builder.otherElementInfo); + } + } else { + otherElementInfo.clear(); + addOtherElementInfo(builder.otherElementInfo); + } + addReferencedTypeNamesToAnnotations(builder.referencedTypeNamesToAnnotations); + addReferencedModuleNames(builder.referencedModuleNames); builder.superTypeInfo().ifPresent(this::superTypeInfo); - addInterfaceTypeInfo(builder.interfaceTypeInfo()); - addModifiers(builder.modifiers()); - addElementModifiers(builder.elementModifiers()); + if (isInterfaceTypeInfoMutated) { + if (builder.isInterfaceTypeInfoMutated) { + addInterfaceTypeInfo(builder.interfaceTypeInfo); + } + } else { + interfaceTypeInfo.clear(); + addInterfaceTypeInfo(builder.interfaceTypeInfo); + } + addModifiers(builder.modifiers); + addElementModifiers(builder.elementModifiers); builder.accessModifier().ifPresent(this::accessModifier); builder.module().ifPresent(this::module); builder.originatingElement().ifPresent(this::originatingElement); - addAnnotations(builder.annotations()); - addInheritedAnnotations(builder.inheritedAnnotations()); + if (isAnnotationsMutated) { + if (builder.isAnnotationsMutated) { + addAnnotations(builder.annotations); + } + } else { + annotations.clear(); + addAnnotations(builder.annotations); + } + if (isInheritedAnnotationsMutated) { + if (builder.isInheritedAnnotationsMutated) { + addInheritedAnnotations(builder.inheritedAnnotations); + } + } else { + inheritedAnnotations.clear(); + addInheritedAnnotations(builder.inheritedAnnotations); + } return self(); } /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @param typeName the type name * @return updated builder instance @@ -158,6 +222,9 @@ public BUILDER typeName(TypeName typeName) { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @param consumer consumer of builder for * the type name @@ -174,6 +241,9 @@ public BUILDER typeName(Consumer consumer) { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @param supplier supplier of * the type name @@ -186,6 +256,107 @@ public BUILDER typeName(Supplier supplier) { return self(); } + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *

      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @param rawType raw type of this type info + * @return updated builder instance + * @see #rawType() + */ + public BUILDER rawType(TypeName rawType) { + Objects.requireNonNull(rawType); + this.rawType = rawType; + return self(); + } + + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *
      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @param consumer consumer of builder for + * raw type of this type info + * @return updated builder instance + * @see #rawType() + */ + public BUILDER rawType(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.rawType(builder.build()); + return self(); + } + + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *
      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @param supplier supplier of + * raw type of this type info + * @return updated builder instance + * @see #rawType() + */ + public BUILDER rawType(Supplier supplier) { + Objects.requireNonNull(supplier); + this.rawType(supplier.get()); + return self(); + } + + /** + * The declared type name, including type parameters. + * + * @param declaredType type name with declared type parameters + * @return updated builder instance + * @see #declaredType() + */ + public BUILDER declaredType(TypeName declaredType) { + Objects.requireNonNull(declaredType); + this.declaredType = declaredType; + return self(); + } + + /** + * The declared type name, including type parameters. + * + * @param consumer consumer of builder for + * type name with declared type parameters + * @return updated builder instance + * @see #declaredType() + */ + public BUILDER declaredType(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.declaredType(builder.build()); + return self(); + } + + /** + * The declared type name, including type parameters. + * + * @param supplier supplier of + * type name with declared type parameters + * @return updated builder instance + * @see #declaredType() + */ + public BUILDER declaredType(Supplier supplier) { + Objects.requireNonNull(supplier); + this.declaredType(supplier.get()); + return self(); + } + /** * Clear existing value of this property. * @@ -262,6 +433,7 @@ public BUILDER kind(ElementKind kind) { */ public BUILDER elementInfo(List elementInfo) { Objects.requireNonNull(elementInfo); + isElementInfoMutated = true; this.elementInfo.clear(); this.elementInfo.addAll(elementInfo); return self(); @@ -276,6 +448,7 @@ public BUILDER elementInfo(List elementInfo) { */ public BUILDER addElementInfo(List elementInfo) { Objects.requireNonNull(elementInfo); + isElementInfoMutated = true; this.elementInfo.addAll(elementInfo); return self(); } @@ -290,6 +463,7 @@ public BUILDER addElementInfo(List elementInfo) { public BUILDER addElementInfo(TypedElementInfo elementInfo) { Objects.requireNonNull(elementInfo); this.elementInfo.add(elementInfo); + isElementInfoMutated = true; return self(); } @@ -318,6 +492,7 @@ public BUILDER addElementInfo(Consumer consumer) { */ public BUILDER otherElementInfo(List otherElementInfo) { Objects.requireNonNull(otherElementInfo); + isOtherElementInfoMutated = true; this.otherElementInfo.clear(); this.otherElementInfo.addAll(otherElementInfo); return self(); @@ -333,6 +508,7 @@ public BUILDER otherElementInfo(List otherElementInf */ public BUILDER addOtherElementInfo(List otherElementInfo) { Objects.requireNonNull(otherElementInfo); + isOtherElementInfoMutated = true; this.otherElementInfo.addAll(otherElementInfo); return self(); } @@ -348,6 +524,7 @@ public BUILDER addOtherElementInfo(List otherElement public BUILDER addOtherElementInfo(TypedElementInfo otherElementInfo) { Objects.requireNonNull(otherElementInfo); this.otherElementInfo.add(otherElementInfo); + isOtherElementInfoMutated = true; return self(); } @@ -374,8 +551,8 @@ public BUILDER addOtherElementInfo(Consumer consumer) * @return updated builder instance * @see #referencedTypeNamesToAnnotations() */ - public BUILDER referencedTypeNamesToAnnotations(Map> referencedTypeNamesToAnnotations) { + public BUILDER referencedTypeNamesToAnnotations( + Map> referencedTypeNamesToAnnotations) { Objects.requireNonNull(referencedTypeNamesToAnnotations); this.referencedTypeNamesToAnnotations.clear(); this.referencedTypeNamesToAnnotations.putAll(referencedTypeNamesToAnnotations); @@ -539,6 +716,7 @@ public BUILDER superTypeInfo(Consumer consumer) { */ public BUILDER interfaceTypeInfo(List interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; this.interfaceTypeInfo.clear(); this.interfaceTypeInfo.addAll(interfaceTypeInfo); return self(); @@ -553,6 +731,7 @@ public BUILDER interfaceTypeInfo(List interfaceTypeInfo) { */ public BUILDER addInterfaceTypeInfo(List interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; this.interfaceTypeInfo.addAll(interfaceTypeInfo); return self(); } @@ -567,6 +746,7 @@ public BUILDER addInterfaceTypeInfo(List interfaceTypeInfo) public BUILDER addInterfaceTypeInfo(TypeInfo interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); this.interfaceTypeInfo.add(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; return self(); } @@ -744,6 +924,7 @@ public BUILDER originatingElement(Object originatingElement) { */ public BUILDER annotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.clear(); this.annotations.addAll(annotations); return self(); @@ -760,6 +941,7 @@ public BUILDER annotations(List annotations) { */ public BUILDER addAnnotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.addAll(annotations); return self(); } @@ -776,6 +958,7 @@ public BUILDER addAnnotations(List annotations) { public BUILDER addAnnotation(Annotation annotation) { Objects.requireNonNull(annotation); this.annotations.add(annotation); + isAnnotationsMutated = true; return self(); } @@ -802,6 +985,8 @@ public BUILDER addAnnotation(Consumer consumer) { *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -809,6 +994,7 @@ public BUILDER addAnnotation(Consumer consumer) { */ public BUILDER inheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.clear(); this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); @@ -820,6 +1006,8 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -827,6 +1015,7 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati */ public BUILDER addInheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); } @@ -837,6 +1026,8 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotation list of all meta annotations of this element * @return updated builder instance @@ -845,6 +1036,7 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { Objects.requireNonNull(inheritedAnnotation); this.inheritedAnnotations.add(inheritedAnnotation); + isInheritedAnnotationsMutated = true; return self(); } @@ -854,6 +1046,8 @@ public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param consumer list of all meta annotations of this element * @return updated builder instance @@ -869,6 +1063,9 @@ public BUILDER addInheritedAnnotation(Consumer consumer) { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @return the type name */ @@ -876,6 +1073,29 @@ public Optional typeName() { return Optional.ofNullable(typeName); } + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *

      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @return the raw type + */ + public Optional rawType() { + return Optional.ofNullable(rawType); + } + + /** + * The declared type name, including type parameters. + * + * @return the declared type + */ + public Optional declaredType() { + return Optional.ofNullable(declaredType); + } + /** * Description, such as javadoc, if available. * @@ -1049,6 +1269,8 @@ public List annotations() { *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @return the inherited annotations */ @@ -1060,6 +1282,8 @@ public List inheritedAnnotations() { public String toString() { return "TypeInfoBuilder{" + "typeName=" + typeName + "," + + "rawType=" + rawType + "," + + "declaredType=" + declaredType + "," + "kind=" + kind + "," + "elementInfo=" + elementInfo + "," + "superTypeInfo=" + superTypeInfo + "," @@ -1086,6 +1310,12 @@ protected void validatePrototype() { if (typeName == null) { collector.fatal(getClass(), "Property \"typeName\" is required, but not set"); } + if (rawType == null) { + collector.fatal(getClass(), "Property \"rawType\" must not be null, but not set"); + } + if (declaredType == null) { + collector.fatal(getClass(), "Property \"declaredType\" must not be null, but not set"); + } if (typeKind == null) { collector.fatal(getClass(), "Property \"typeKind\" is required, but not set"); } @@ -1173,6 +1403,8 @@ protected static class TypeInfoImpl implements TypeInfo { private final Set elementModifiers; private final Set modifiers; private final String typeKind; + private final TypeName declaredType; + private final TypeName rawType; private final TypeName typeName; /** @@ -1182,6 +1414,8 @@ protected static class TypeInfoImpl implements TypeInfo { */ protected TypeInfoImpl(TypeInfo.BuilderBase builder) { this.typeName = builder.typeName().get(); + this.rawType = builder.rawType().get(); + this.declaredType = builder.declaredType().get(); this.description = builder.description(); this.typeKind = builder.typeKind().get(); this.kind = builder.kind().get(); @@ -1206,6 +1440,16 @@ public TypeName typeName() { return typeName; } + @Override + public TypeName rawType() { + return rawType; + } + + @Override + public TypeName declaredType() { + return declaredType; + } + @Override public Optional description() { return description; @@ -1290,6 +1534,8 @@ public List inheritedAnnotations() { public String toString() { return "TypeInfo{" + "typeName=" + typeName + "," + + "rawType=" + rawType + "," + + "declaredType=" + declaredType + "," + "kind=" + kind + "," + "elementInfo=" + elementInfo + "," + "superTypeInfo=" + superTypeInfo + "," @@ -1310,6 +1556,8 @@ public boolean equals(Object o) { return false; } return Objects.equals(typeName, other.typeName()) + && Objects.equals(rawType, other.rawType()) + && Objects.equals(declaredType, other.declaredType()) && Objects.equals(kind, other.kind()) && Objects.equals(elementInfo, other.elementInfo()) && Objects.equals(superTypeInfo, other.superTypeInfo()) @@ -1323,6 +1571,8 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(typeName, + rawType, + declaredType, kind, elementInfo, superTypeInfo, diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 4bb3da96300..e744bb97bae 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -16,10 +16,13 @@ package io.helidon.common.types; +import java.util.ArrayDeque; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import io.helidon.builder.api.Option; @@ -32,12 +35,34 @@ interface TypeInfoBlueprint extends Annotated { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @return the type name */ @Option.Required TypeName typeName(); + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *

      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @return raw type of this type info + */ + TypeName rawType(); + + /** + * The declared type name, including type parameters. + * + * @return type name with declared type parameters + */ + TypeName declaredType(); + /** * Description, such as javadoc, if available. * @@ -228,6 +253,54 @@ default Optional metaAnnotation(TypeName annotation, TypeName metaAn @Option.Redundant Optional originatingElement(); + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypeInfo#typeName()} if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code ClassInfo} when using classpath scanning. + * + * @return originating element, or the type of this type info + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::typeName); + } + + /** + * Checks if the current type implements, or extends the provided type. + * This method analyzes the whole dependency tree of the current type. + * + * @param typeName type of interface to check + * @return the super type info, or interface type info matching the provided type, with appropriate generic declarations + */ + default Optional findInHierarchy(TypeName typeName) { + if (typeName.equals(typeName())) { + return Optional.of((TypeInfo) this); + } + // scan super types + Optional superClass = superTypeInfo(); + if (superClass.isPresent() && !superClass.get().typeName().equals(TypeNames.OBJECT)) { + var superType = superClass.get(); + var foundInSuper = superType.findInHierarchy(typeName); + if (foundInSuper.isPresent()) { + return foundInSuper; + } + } + // nope, let's try interfaces + Queue interfaces = new ArrayDeque<>(interfaceTypeInfo()); + Set processed = new HashSet<>(); + + while (!interfaces.isEmpty()) { + TypeInfo type = interfaces.remove(); + // make sure we process each type only once + if (processed.add(type.typeName())) { + if (typeName.equals(type.typeName())) { + return Optional.of(type); + } + interfaces.addAll(type.interfaceTypeInfo()); + } + } + return Optional.empty(); + } + /** * Uses {@link io.helidon.common.types.TypeInfo#referencedModuleNames()} to determine if the module name is known for the * given type. diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java index a054c002053..952cd31cc0e 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,18 @@ public void decorate(TypeInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); + + // new methods, simplify for tests + if (target.rawType().isEmpty()) { + target.typeName() + .map(TypeName::genericTypeName) + .ifPresent(target::rawType); + } + if (target.declaredType().isEmpty()) { + // this may not be correct, but is correct for all types that do not have any declaration of generics + // so it simplifies a lot of use cases + target.rawType().ifPresent(target::declaredType); + } } } } diff --git a/common/types/src/main/java/io/helidon/common/types/TypeName.java b/common/types/src/main/java/io/helidon/common/types/TypeName.java index 188ce614f23..a37c0e2cca6 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeName.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,11 +125,18 @@ static TypeName createFromGenericDeclaration(String genericAliasTypeName) { abstract class BuilderBase, PROTOTYPE extends TypeName> implements Prototype.Builder { + private final List lowerBounds = new ArrayList<>(); private final List typeArguments = new ArrayList<>(); + private final List upperBounds = new ArrayList<>(); private final List enclosingNames = new ArrayList<>(); private final List typeParameters = new ArrayList<>(); private boolean array = false; private boolean generic = false; + private boolean isEnclosingNamesMutated; + private boolean isLowerBoundsMutated; + private boolean isTypeArgumentsMutated; + private boolean isTypeParametersMutated; + private boolean isUpperBoundsMutated; private boolean primitive = false; private boolean wildcard = false; private String className; @@ -142,7 +149,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -150,13 +157,30 @@ protected BuilderBase() { public BUILDER from(TypeName prototype) { packageName(prototype.packageName()); className(prototype.className()); + if (!isEnclosingNamesMutated) { + enclosingNames.clear(); + } addEnclosingNames(prototype.enclosingNames()); primitive(prototype.primitive()); array(prototype.array()); generic(prototype.generic()); wildcard(prototype.wildcard()); + if (!isTypeArgumentsMutated) { + typeArguments.clear(); + } addTypeArguments(prototype.typeArguments()); + if (!isTypeParametersMutated) { + typeParameters.clear(); + } addTypeParameters(prototype.typeParameters()); + if (!isLowerBoundsMutated) { + lowerBounds.clear(); + } + addLowerBounds(prototype.lowerBounds()); + if (!isUpperBoundsMutated) { + upperBounds.clear(); + } + addUpperBounds(prototype.upperBounds()); return self(); } @@ -169,20 +193,58 @@ public BUILDER from(TypeName prototype) { public BUILDER from(TypeName.BuilderBase builder) { packageName(builder.packageName()); builder.className().ifPresent(this::className); - addEnclosingNames(builder.enclosingNames()); + if (isEnclosingNamesMutated) { + if (builder.isEnclosingNamesMutated) { + addEnclosingNames(builder.enclosingNames); + } + } else { + enclosingNames.clear(); + addEnclosingNames(builder.enclosingNames); + } primitive(builder.primitive()); array(builder.array()); generic(builder.generic()); wildcard(builder.wildcard()); - addTypeArguments(builder.typeArguments()); - addTypeParameters(builder.typeParameters()); + if (isTypeArgumentsMutated) { + if (builder.isTypeArgumentsMutated) { + addTypeArguments(builder.typeArguments); + } + } else { + typeArguments.clear(); + addTypeArguments(builder.typeArguments); + } + if (isTypeParametersMutated) { + if (builder.isTypeParametersMutated) { + addTypeParameters(builder.typeParameters); + } + } else { + typeParameters.clear(); + addTypeParameters(builder.typeParameters); + } + if (isLowerBoundsMutated) { + if (builder.isLowerBoundsMutated) { + addLowerBounds(builder.lowerBounds); + } + } else { + lowerBounds.clear(); + addLowerBounds(builder.lowerBounds); + } + if (isUpperBoundsMutated) { + if (builder.isUpperBoundsMutated) { + addUpperBounds(builder.upperBounds); + } + } else { + upperBounds.clear(); + addUpperBounds(builder.upperBounds); + } return self(); } /** * Update builder from the provided type. * - * @param type type to get information (package name, class name, primitive, array) + * @param type type to get information (package name, class name, primitive, array), can only be a class or a + * {@link io.helidon.common.GenericType} * @return updated builder instance */ public BUILDER type(Type type) { @@ -227,6 +289,7 @@ public BUILDER className(String className) { */ public BUILDER enclosingNames(List enclosingNames) { Objects.requireNonNull(enclosingNames); + isEnclosingNamesMutated = true; this.enclosingNames.clear(); this.enclosingNames.addAll(enclosingNames); return self(); @@ -243,6 +306,7 @@ public BUILDER enclosingNames(List enclosingNames) { */ public BUILDER addEnclosingNames(List enclosingNames) { Objects.requireNonNull(enclosingNames); + isEnclosingNamesMutated = true; this.enclosingNames.addAll(enclosingNames); return self(); } @@ -259,6 +323,7 @@ public BUILDER addEnclosingNames(List enclosingNames) { public BUILDER addEnclosingName(String enclosingName) { Objects.requireNonNull(enclosingName); this.enclosingNames.add(enclosingName); + isEnclosingNamesMutated = true; return self(); } @@ -319,6 +384,7 @@ public BUILDER wildcard(boolean wildcard) { */ public BUILDER typeArguments(List typeArguments) { Objects.requireNonNull(typeArguments); + isTypeArgumentsMutated = true; this.typeArguments.clear(); this.typeArguments.addAll(typeArguments); return self(); @@ -333,6 +399,7 @@ public BUILDER typeArguments(List typeArguments) { */ public BUILDER addTypeArguments(List typeArguments) { Objects.requireNonNull(typeArguments); + isTypeArgumentsMutated = true; this.typeArguments.addAll(typeArguments); return self(); } @@ -348,6 +415,7 @@ public BUILDER addTypeArguments(List typeArguments) { public BUILDER addTypeArgument(TypeName typeArgument) { Objects.requireNonNull(typeArgument); this.typeArguments.add(typeArgument); + isTypeArgumentsMutated = true; return self(); } @@ -378,6 +446,7 @@ public BUILDER addTypeArgument(Consumer consumer) { */ public BUILDER typeParameters(List typeParameters) { Objects.requireNonNull(typeParameters); + isTypeParametersMutated = true; this.typeParameters.clear(); this.typeParameters.addAll(typeParameters); return self(); @@ -394,6 +463,7 @@ public BUILDER typeParameters(List typeParameters) { */ public BUILDER addTypeParameters(List typeParameters) { Objects.requireNonNull(typeParameters); + isTypeParametersMutated = true; this.typeParameters.addAll(typeParameters); return self(); } @@ -405,11 +475,160 @@ public BUILDER addTypeParameters(List typeParameters) { * * @param typeParameter type parameter names as declared on this type, or names that represent the {@link #typeArguments()} * @return updated builder instance + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information * @see #typeParameters() */ + @Deprecated(since = "4.2.0", forRemoval = true) public BUILDER addTypeParameter(String typeParameter) { Objects.requireNonNull(typeParameter); this.typeParameters.add(typeParameter); + isTypeParametersMutated = true; + return self(); + } + + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param lowerBounds list of lower bounds of this type + * @return updated builder instance + * @see #lowerBounds() + */ + public BUILDER lowerBounds(List lowerBounds) { + Objects.requireNonNull(lowerBounds); + isLowerBoundsMutated = true; + this.lowerBounds.clear(); + this.lowerBounds.addAll(lowerBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param lowerBounds list of lower bounds of this type + * @return updated builder instance + * @see #lowerBounds() + */ + public BUILDER addLowerBounds(List lowerBounds) { + Objects.requireNonNull(lowerBounds); + isLowerBoundsMutated = true; + this.lowerBounds.addAll(lowerBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param lowerBound list of lower bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #lowerBounds() + */ + public BUILDER addLowerBound(TypeName lowerBound) { + Objects.requireNonNull(lowerBound); + this.lowerBounds.add(lowerBound); + isLowerBoundsMutated = true; + return self(); + } + + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param consumer list of lower bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #lowerBounds() + * @see #lowerBounds() + */ + public BUILDER addLowerBound(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.lowerBounds.add(builder.build()); + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param upperBounds list of upper bounds of this type + * @return updated builder instance + * @see #upperBounds() + */ + public BUILDER upperBounds(List upperBounds) { + Objects.requireNonNull(upperBounds); + isUpperBoundsMutated = true; + this.upperBounds.clear(); + this.upperBounds.addAll(upperBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param upperBounds list of upper bounds of this type + * @return updated builder instance + * @see #upperBounds() + */ + public BUILDER addUpperBounds(List upperBounds) { + Objects.requireNonNull(upperBounds); + isUpperBoundsMutated = true; + this.upperBounds.addAll(upperBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param upperBound list of upper bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #upperBounds() + */ + public BUILDER addUpperBound(TypeName upperBound) { + Objects.requireNonNull(upperBound); + this.upperBounds.add(upperBound); + isUpperBoundsMutated = true; + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param consumer list of upper bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #upperBounds() + * @see #upperBounds() + */ + public BUILDER addUpperBound(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.upperBounds.add(builder.build()); return self(); } @@ -495,15 +714,48 @@ public List typeArguments() { * if {@link #typeArguments()} exist, this list MUST exist and have the same size and order (it maps the name to the type). * * @return the type parameters + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information */ + @Deprecated(since = "4.2.0", forRemoval = true) public List typeParameters() { return typeParameters; } + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return the lower bounds + * @see io.helidon.common.types.TypeName#generic() + * @see #lowerBounds() + * @see #lowerBounds() + */ + public List lowerBounds() { + return lowerBounds; + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return the upper bounds + * @see io.helidon.common.types.TypeName#generic() + * @see #upperBounds() + * @see #upperBounds() + */ + public List upperBounds() { + return upperBounds; + } + /** * Handles providers and decorators. */ protected void preBuildPrototype() { + new TypeNameSupport.Decorator().decorate(this); } /** @@ -526,7 +778,9 @@ protected static class TypeNameImpl implements TypeName { private final boolean generic; private final boolean primitive; private final boolean wildcard; + private final List lowerBounds; private final List typeArguments; + private final List upperBounds; private final List enclosingNames; private final List typeParameters; private final String className; @@ -547,6 +801,8 @@ protected TypeNameImpl(TypeName.BuilderBase builder) { this.wildcard = builder.wildcard(); this.typeArguments = List.copyOf(builder.typeArguments()); this.typeParameters = List.copyOf(builder.typeParameters()); + this.lowerBounds = List.copyOf(builder.lowerBounds()); + this.upperBounds = List.copyOf(builder.upperBounds()); } @Override @@ -629,6 +885,16 @@ public List typeParameters() { return typeParameters; } + @Override + public List lowerBounds() { + return lowerBounds; + } + + @Override + public List upperBounds() { + return upperBounds; + } + @Override public boolean equals(Object o) { if (o == this) { diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java index 1d9d5fa7118..d712863b5b4 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java @@ -44,7 +44,7 @@ *

  • {@link #declaredName()} and {@link #resolvedName()}.
  • * */ -@Prototype.Blueprint +@Prototype.Blueprint(decorator = TypeNameSupport.Decorator.class) @Prototype.CustomMethods(TypeNameSupport.class) @Prototype.Implement("java.lang.Comparable") interface TypeNameBlueprint { @@ -137,11 +137,39 @@ default String classNameWithEnclosingNames() { * if {@link #typeArguments()} exist, this list MUST exist and have the same size and order (it maps the name to the type). * * @return type parameter names as declared on this type, or names that represent the {@link #typeArguments()} + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information */ @Option.Singular @Option.Redundant + @Deprecated(forRemoval = true, since = "4.2.0") List typeParameters(); + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of lower bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List lowerBounds(); + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of upper bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List upperBounds(); + /** * Indicates whether this type is a {@code java.util.List}. * diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java b/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java index e8a9d38809b..791acb5f485 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java @@ -16,16 +16,20 @@ package io.helidon.common.types; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.helidon.builder.api.Prototype; +import io.helidon.common.GenericType; final class TypeNameSupport { private static final TypeName PRIMITIVE_BOOLEAN = TypeName.create(boolean.class); @@ -141,59 +145,62 @@ static String fqName(TypeName instance) { @Prototype.PrototypeMethod @Prototype.Annotated("java.lang.Override") // defined on blueprint static String resolvedName(TypeName instance) { - String name = calcName(instance, "."); - boolean isObject = Object.class.getName().equals(name) || "?".equals(name); - StringBuilder nameBuilder = (isObject) - ? new StringBuilder(instance.wildcard() ? "?" : name) - : new StringBuilder(instance.wildcard() ? "? extends " + name : name); - - if (!instance.typeArguments().isEmpty()) { - nameBuilder.append("<"); - int i = 0; - for (TypeName param : instance.typeArguments()) { - if (i > 0) { - nameBuilder.append(", "); - } - nameBuilder.append(param.resolvedName()); - i++; - } - nameBuilder.append(">"); + if (instance.generic() || instance.wildcard()) { + return resolveGenericName(instance); } - - if (instance.array()) { - nameBuilder.append("[]"); - } - - return nameBuilder.toString(); + return resolveClassName(instance); } /** * Update builder from the provided type. * * @param builder builder to update - * @param type type to get information (package name, class name, primitive, array) + * @param type type to get information (package name, class name, primitive, array) */ @Prototype.BuilderMethod static void type(TypeName.BuilderBase builder, Type type) { Objects.requireNonNull(type); if (type instanceof Class classType) { - Class componentType = classType.isArray() ? classType.getComponentType() : classType; - builder.packageName(componentType.getPackageName()); - builder.className(componentType.getSimpleName()); - builder.primitive(componentType.isPrimitive()); - builder.array(classType.isArray()); - - Class enclosingClass = classType.getEnclosingClass(); - LinkedList enclosingTypes = new LinkedList<>(); - while (enclosingClass != null) { - enclosingTypes.addFirst(enclosingClass.getSimpleName()); - enclosingClass = enclosingClass.getEnclosingClass(); + updateFromClass(builder, classType); + return; + } + Type reflectGenericType = type; + + if (type instanceof GenericType gt) { + if (gt.isClass()) { + // simple case - just a class + updateFromClass(builder, gt.rawType()); + return; + } else { + // complex case - has generic type arguments + reflectGenericType = gt.type(); } - builder.enclosingNames(enclosingTypes); - } else { - // todo - throw new IllegalArgumentException("Currently we only support class as a parameter, but got: " + type); } + + // translate the generic type into type name + if (reflectGenericType instanceof ParameterizedType pt) { + Type raw = pt.getRawType(); + if (raw instanceof Class theClass) { + updateFromClass(builder, theClass); + } else { + throw new IllegalArgumentException("Raw type of a ParameterizedType is not a class: " + raw.getClass().getName() + + ", for " + pt.getTypeName()); + } + + Type[] actualTypeArguments = pt.getActualTypeArguments(); + for (Type actualTypeArgument : actualTypeArguments) { + builder.addTypeArgument(TypeName.create(actualTypeArgument)); + } + return; + } + if (reflectGenericType instanceof WildcardType) { + builder.className("?"); + builder.wildcard(true); + return; + } + + throw new IllegalArgumentException("We can only create a type from a class, GenericType, or a ParameterizedType," + + " but got: " + reflectGenericType.getClass().getName()); } /** @@ -309,6 +316,71 @@ static TypeName createFromGenericDeclaration(String genericAliasTypeName) { .build(); } + private static String resolveGenericName(TypeName instance) { + // ?, ? super Something; ? extends Something + String prefix = instance.wildcard() ? "?" : instance.className(); + if (instance.upperBounds().isEmpty() && instance.lowerBounds().isEmpty()) { + return prefix; + } + if (instance.lowerBounds().isEmpty()) { + return prefix + " extends " + instance.upperBounds() + .stream() + .map(it -> { + if (it.generic()) { + return it.wildcard() ? "?" : it.className(); + } + return it.resolvedName(); + }) + .collect(Collectors.joining(" & ")); + } + TypeName lowerBound = instance.lowerBounds().getFirst(); + if (lowerBound.generic()) { + return prefix + " super " + (lowerBound.wildcard() ? "?" : lowerBound.className()); + } + return prefix + " super " + lowerBound.resolvedName(); + + } + + private static String resolveClassName(TypeName instance) { + String name = calcName(instance, "."); + StringBuilder nameBuilder = new StringBuilder(name); + + if (!instance.typeArguments().isEmpty()) { + nameBuilder.append("<"); + int i = 0; + for (TypeName param : instance.typeArguments()) { + if (i > 0) { + nameBuilder.append(", "); + } + nameBuilder.append(param.resolvedName()); + i++; + } + nameBuilder.append(">"); + } + + if (instance.array()) { + nameBuilder.append("[]"); + } + + return nameBuilder.toString(); + } + + private static void updateFromClass(TypeName.BuilderBase builder, Class classType) { + Class componentType = classType.isArray() ? classType.getComponentType() : classType; + builder.packageName(componentType.getPackageName()); + builder.className(componentType.getSimpleName()); + builder.primitive(componentType.isPrimitive()); + builder.array(classType.isArray()); + + Class enclosingClass = classType.getEnclosingClass(); + LinkedList enclosingTypes = new LinkedList<>(); + while (enclosingClass != null) { + enclosingTypes.addFirst(enclosingClass.getSimpleName()); + enclosingClass = enclosingClass.getEnclosingClass(); + } + builder.enclosingNames(enclosingTypes); + } + private static String calcName(TypeName instance, String typeSeparator) { String className; if (instance.enclosingNames().isEmpty()) { @@ -320,4 +392,38 @@ private static String calcName(TypeName instance, String typeSeparator) { return (instance.primitive() || instance.packageName().isEmpty()) ? className : instance.packageName() + "." + className; } + + static class Decorator implements Prototype.BuilderDecorator> { + @Override + public void decorate(TypeName.BuilderBase target) { + fixWildcards(target); + } + + private void fixWildcards(TypeName.BuilderBase target) { + // handle wildcards correct + if (target.wildcard()) { + if (target.upperBounds().size() == 1 && target.lowerBounds().isEmpty()) { + // backward compatible for (? extends X) + TypeName upperBound = target.upperBounds().getFirst(); + target.className(upperBound.className()); + target.packageName(upperBound.packageName()); + target.enclosingNames(upperBound.enclosingNames()); + } + // wildcard set, if package + class name as well, set them as upper bounds + if (target.className().isPresent() + && !target.className().get().equals("?") + && target.upperBounds().isEmpty() + && target.lowerBounds().isEmpty()) { + TypeName upperBound = TypeName.builder() + .from(target) + .wildcard(false) + .build(); + if (!upperBound.equals(TypeNames.OBJECT)) { + target.addUpperBound(upperBound); + } + } + target.generic(true); + } + } + } } diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNames.java b/common/types/src/main/java/io/helidon/common/types/TypeNames.java index 62fe59da5a3..b3d513c516e 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNames.java @@ -16,8 +16,10 @@ package io.helidon.common.types; +import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -28,6 +30,7 @@ import io.helidon.common.Generated; import io.helidon.common.GenericType; +import io.helidon.common.Size; /** * Commonly used type names. @@ -73,10 +76,25 @@ public final class TypeNames { * Type name for {@link java.lang.annotation.Retention}. */ public static final TypeName RETENTION = TypeName.create(Retention.class); + /** + * Type name for {@link java.lang.annotation.Documented}. + */ + public static final TypeName DOCUMENTED = TypeName.create(Documented.class); /** * Type name for {@link java.lang.annotation.Inherited}. */ public static final TypeName INHERITED = TypeName.create(Inherited.class); + /** + * Type name for {@link java.lang.annotation.Target}. + */ + public static final TypeName TARGET = TypeName.create(Target.class); + /** + * Wildcard type name, represented in code by {@code ?}. + */ + public static final TypeName WILDCARD = TypeName.builder() + .className("?") + .wildcard(true) + .build(); /* Primitive types and their boxed counterparts @@ -161,6 +179,10 @@ public final class TypeNames { * Type name of the type name. */ public static final TypeName TYPE_NAME = TypeName.create(TypeName.class); + /** + * Type name of the resolved type name. + */ + public static final TypeName RESOLVED_TYPE_NAME = TypeName.create(ResolvedType.class); /** * Type name of typed element info. */ @@ -185,6 +207,10 @@ public final class TypeNames { * Helidon {@link io.helidon.common.GenericType}. */ public static final TypeName GENERIC_TYPE = TypeName.create(GenericType.class); + /** + * Type name for {@link io.helidon.common.Size}. + */ + public static final TypeName SIZE = TypeName.create(Size.class); private TypeNames() { } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java index 905b540268f..9560152bd7e 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java @@ -79,7 +79,13 @@ abstract class BuilderBase elementModifiers = new LinkedHashSet<>(); private final Set modifiers = new LinkedHashSet<>(); private AccessModifier accessModifier; + private boolean isAnnotationsMutated; + private boolean isComponentTypesMutated; + private boolean isElementTypeAnnotationsMutated; + private boolean isInheritedAnnotationsMutated; + private boolean isParameterArgumentsMutated; private ElementKind kind; + private ElementSignature signature; private Object originatingElement; private String defaultValue; private String description; @@ -95,7 +101,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -107,16 +113,32 @@ public BUILDER from(TypedElementInfo prototype) { elementTypeKind(prototype.elementTypeKind()); kind(prototype.kind()); defaultValue(prototype.defaultValue()); + if (!isElementTypeAnnotationsMutated) { + elementTypeAnnotations.clear(); + } addElementTypeAnnotations(prototype.elementTypeAnnotations()); + if (!isComponentTypesMutated) { + componentTypes.clear(); + } addComponentTypes(prototype.componentTypes()); addModifiers(prototype.modifiers()); addElementModifiers(prototype.elementModifiers()); accessModifier(prototype.accessModifier()); enclosingType(prototype.enclosingType()); + if (!isParameterArgumentsMutated) { + parameterArguments.clear(); + } addParameterArguments(prototype.parameterArguments()); addThrowsChecked(prototype.throwsChecked()); originatingElement(prototype.originatingElement()); + signature(prototype.signature()); + if (!isAnnotationsMutated) { + annotations.clear(); + } addAnnotations(prototype.annotations()); + if (!isInheritedAnnotationsMutated) { + inheritedAnnotations.clear(); + } addInheritedAnnotations(prototype.inheritedAnnotations()); return self(); } @@ -134,17 +156,53 @@ public BUILDER from(TypedElementInfo.BuilderBase builder) { builder.elementTypeKind().ifPresent(this::elementTypeKind); builder.kind().ifPresent(this::kind); builder.defaultValue().ifPresent(this::defaultValue); - addElementTypeAnnotations(builder.elementTypeAnnotations()); - addComponentTypes(builder.componentTypes()); - addModifiers(builder.modifiers()); - addElementModifiers(builder.elementModifiers()); + if (isElementTypeAnnotationsMutated) { + if (builder.isElementTypeAnnotationsMutated) { + addElementTypeAnnotations(builder.elementTypeAnnotations); + } + } else { + elementTypeAnnotations.clear(); + addElementTypeAnnotations(builder.elementTypeAnnotations); + } + if (isComponentTypesMutated) { + if (builder.isComponentTypesMutated) { + addComponentTypes(builder.componentTypes); + } + } else { + componentTypes.clear(); + addComponentTypes(builder.componentTypes); + } + addModifiers(builder.modifiers); + addElementModifiers(builder.elementModifiers); builder.accessModifier().ifPresent(this::accessModifier); builder.enclosingType().ifPresent(this::enclosingType); - addParameterArguments(builder.parameterArguments()); - addThrowsChecked(builder.throwsChecked()); + if (isParameterArgumentsMutated) { + if (builder.isParameterArgumentsMutated) { + addParameterArguments(builder.parameterArguments); + } + } else { + parameterArguments.clear(); + addParameterArguments(builder.parameterArguments); + } + addThrowsChecked(builder.throwsChecked); builder.originatingElement().ifPresent(this::originatingElement); - addAnnotations(builder.annotations()); - addInheritedAnnotations(builder.inheritedAnnotations()); + builder.signature().ifPresent(this::signature); + if (isAnnotationsMutated) { + if (builder.isAnnotationsMutated) { + addAnnotations(builder.annotations); + } + } else { + annotations.clear(); + addAnnotations(builder.annotations); + } + if (isInheritedAnnotationsMutated) { + if (builder.isInheritedAnnotationsMutated) { + addInheritedAnnotations(builder.inheritedAnnotations); + } + } else { + inheritedAnnotations.clear(); + addInheritedAnnotations(builder.inheritedAnnotations); + } return self(); } @@ -286,7 +344,7 @@ public BUILDER defaultValue(String defaultValue) { } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @param elementTypeAnnotations the list of annotations on this element's (return) type. * @return updated builder instance @@ -294,13 +352,14 @@ public BUILDER defaultValue(String defaultValue) { */ public BUILDER elementTypeAnnotations(List elementTypeAnnotations) { Objects.requireNonNull(elementTypeAnnotations); + isElementTypeAnnotationsMutated = true; this.elementTypeAnnotations.clear(); this.elementTypeAnnotations.addAll(elementTypeAnnotations); return self(); } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @param elementTypeAnnotations the list of annotations on this element's (return) type. * @return updated builder instance @@ -308,6 +367,7 @@ public BUILDER elementTypeAnnotations(List elementTypeAnno */ public BUILDER addElementTypeAnnotations(List elementTypeAnnotations) { Objects.requireNonNull(elementTypeAnnotations); + isElementTypeAnnotationsMutated = true; this.elementTypeAnnotations.addAll(elementTypeAnnotations); return self(); } @@ -321,6 +381,7 @@ public BUILDER addElementTypeAnnotations(List elementTypeA */ public BUILDER componentTypes(List componentTypes) { Objects.requireNonNull(componentTypes); + isComponentTypesMutated = true; this.componentTypes.clear(); this.componentTypes.addAll(componentTypes); return self(); @@ -335,6 +396,7 @@ public BUILDER componentTypes(List componentTypes) { */ public BUILDER addComponentTypes(List componentTypes) { Objects.requireNonNull(componentTypes); + isComponentTypesMutated = true; this.componentTypes.addAll(componentTypes); return self(); } @@ -493,6 +555,7 @@ public BUILDER enclosingType(Consumer consumer) { */ public BUILDER parameterArguments(List parameterArguments) { Objects.requireNonNull(parameterArguments); + isParameterArgumentsMutated = true; this.parameterArguments.clear(); this.parameterArguments.addAll(parameterArguments); return self(); @@ -509,6 +572,7 @@ public BUILDER parameterArguments(List parameterArgu */ public BUILDER addParameterArguments(List parameterArguments) { Objects.requireNonNull(parameterArguments); + isParameterArgumentsMutated = true; this.parameterArguments.addAll(parameterArguments); return self(); } @@ -525,6 +589,7 @@ public BUILDER addParameterArguments(List parameterA public BUILDER addParameterArgument(TypedElementInfo parameterArgument) { Objects.requireNonNull(parameterArgument); this.parameterArguments.add(parameterArgument); + isParameterArgumentsMutated = true; return self(); } @@ -609,6 +674,7 @@ public BUILDER originatingElement(Object originatingElement) { */ public BUILDER annotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.clear(); this.annotations.addAll(annotations); return self(); @@ -625,6 +691,7 @@ public BUILDER annotations(List annotations) { */ public BUILDER addAnnotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.addAll(annotations); return self(); } @@ -641,6 +708,7 @@ public BUILDER addAnnotations(List annotations) { public BUILDER addAnnotation(Annotation annotation) { Objects.requireNonNull(annotation); this.annotations.add(annotation); + isAnnotationsMutated = true; return self(); } @@ -667,6 +735,8 @@ public BUILDER addAnnotation(Consumer consumer) { *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -674,6 +744,7 @@ public BUILDER addAnnotation(Consumer consumer) { */ public BUILDER inheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.clear(); this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); @@ -685,6 +756,8 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -692,6 +765,7 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati */ public BUILDER addInheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); } @@ -702,6 +776,8 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotation list of all meta annotations of this element * @return updated builder instance @@ -710,6 +786,7 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { Objects.requireNonNull(inheritedAnnotation); this.inheritedAnnotations.add(inheritedAnnotation); + isInheritedAnnotationsMutated = true; return self(); } @@ -719,6 +796,8 @@ public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @param consumer list of all meta annotations of this element * @return updated builder instance @@ -794,7 +873,7 @@ public Optional defaultValue() { } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @return the element type annotations */ @@ -888,6 +967,17 @@ public Optional originatingElement() { return Optional.ofNullable(originatingElement); } + /** + * Signature of this element. + * + * @return the signature + * @see io.helidon.common.types.ElementSignature + * @see #signature() + */ + public Optional signature() { + return Optional.ofNullable(signature); + } + /** * List of declared and known annotations for this element. * Note that "known" implies that the annotation is visible, which depends @@ -905,6 +995,8 @@ public List annotations() { *

    * The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

    + * This method does not return annotations on super types or interfaces! * * @return the inherited annotations */ @@ -939,6 +1031,9 @@ protected void validatePrototype() { if (accessModifier == null) { collector.fatal(getClass(), "Property \"accessModifier\" must not be null, but not set"); } + if (signature == null) { + collector.fatal(getClass(), "Property \"signature\" must not be null, but not set"); + } collector.collect().checkValid(); } @@ -999,6 +1094,20 @@ BUILDER originatingElement(Optional originatingElement) { return self(); } + /** + * Signature of this element. + * + * @param signature signature of this element + * @return updated builder instance + * @see io.helidon.common.types.ElementSignature + * @see #signature() + */ + BUILDER signature(ElementSignature signature) { + Objects.requireNonNull(signature); + this.signature = signature; + return self(); + } + /** * Generated implementation of the prototype, can be extended by descendant prototype implementations. */ @@ -1006,6 +1115,7 @@ protected static class TypedElementInfoImpl implements TypedElementInfo { private final AccessModifier accessModifier; private final ElementKind kind; + private final ElementSignature signature; private final List annotations; private final List elementTypeAnnotations; private final List inheritedAnnotations; @@ -1043,6 +1153,7 @@ protected TypedElementInfoImpl(TypedElementInfo.BuilderBase builder) { this.parameterArguments = List.copyOf(builder.parameterArguments()); this.throwsChecked = Collections.unmodifiableSet(new LinkedHashSet<>(builder.throwsChecked())); this.originatingElement = builder.originatingElement(); + this.signature = builder.signature().get(); this.annotations = List.copyOf(builder.annotations()); this.inheritedAnnotations = List.copyOf(builder.inheritedAnnotations()); } @@ -1132,6 +1243,11 @@ public Optional originatingElement() { return originatingElement; } + @Override + public ElementSignature signature() { + return signature; + } + @Override public List annotations() { return annotations; @@ -1156,6 +1272,7 @@ public boolean equals(Object o) { && Objects.equals(enclosingType, other.enclosingType()) && Objects.equals(parameterArguments, other.parameterArguments()) && Objects.equals(throwsChecked, other.throwsChecked()) + && Objects.equals(signature, other.signature()) && Objects.equals(annotations, other.annotations()) && Objects.equals(inheritedAnnotations, other.inheritedAnnotations()); } @@ -1168,6 +1285,7 @@ public int hashCode() { enclosingType, parameterArguments, throwsChecked, + signature, annotations, inheritedAnnotations); } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java index b52ba88a4bd..3de312eb13c 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java @@ -167,4 +167,25 @@ interface TypedElementInfoBlueprint extends Annotated { */ @Option.Redundant Optional originatingElement(); + + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypedElementInfo#signature()} + * if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code MethodInfo} (and such) when using classpath scanning. + * + * @return originating element, or the signature of this element + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::signature); + } + + /** + * Signature of this element. + * + * @return signature of this element + * @see io.helidon.common.types.ElementSignature + */ + @Option.Access("") + ElementSignature signature(); } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java index 9edf23abd11..0cba7edaff8 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; @@ -60,10 +61,67 @@ static class BuilderDecorator implements Prototype.BuilderDecorator target) { -/* + backwardCompatibility(target); + constructorName(target); + signature(target); + } + + private void signature(TypedElementInfo.BuilderBase target) { + if (target.kind().isEmpty()) { + // this will fail when validating + target.signature(ElementSignatures.createNone()); + return; + } else { + target.signature(signature(target, target.kind().get())); + } + } + + private ElementSignature signature(TypedElementInfo.BuilderBase target, ElementKind elementKind) { + if (elementKind == ElementKind.CONSTRUCTOR) { + return ElementSignatures.createConstructor(toTypes(target.parameterArguments())); + } + // for everything else we need the type (it is required) + if (target.typeName().isEmpty() || target.elementName().isEmpty()) { + return ElementSignatures.createNone(); + } + + TypeName typeName = target.typeName().get(); + String name = target.elementName().get(); + + if (elementKind == ElementKind.FIELD + || elementKind == ElementKind.RECORD_COMPONENT + || elementKind == ElementKind.ENUM_CONSTANT) { + return ElementSignatures.createField(typeName, name); + } + if (elementKind == ElementKind.METHOD) { + return ElementSignatures.createMethod(typeName, name, toTypes(target.parameterArguments())); + } + if (elementKind == ElementKind.PARAMETER) { + return ElementSignatures.createParameter(typeName, name); + } + return ElementSignatures.createNone(); + } + + private List toTypes(List typedElementInfos) { + return typedElementInfos.stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()); + } + + private void constructorName(TypedElementInfo.BuilderBase target) { + Optional elementKind = target.kind(); + if (elementKind.isPresent()) { + if (elementKind.get() == ElementKind.CONSTRUCTOR) { + target.elementName(""); + } + } + } + + @SuppressWarnings("removal") + private void backwardCompatibility(TypedElementInfo.BuilderBase target) { + /* Backward compatibility for deprecated methods. */ if (target.kind().isEmpty() && target.elementTypeKind().isPresent()) { @@ -103,14 +161,6 @@ public void decorate(TypedElementInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); - - - Optional elementKind = target.kind(); - if (elementKind.isPresent()) { - if (elementKind.get() == ElementKind.CONSTRUCTOR) { - target.elementName(""); - } - } } } } diff --git a/common/types/src/test/java/io/helidon/common/types/SignatureTest.java b/common/types/src/test/java/io/helidon/common/types/SignatureTest.java new file mode 100644 index 00000000000..2dce94997b9 --- /dev/null +++ b/common/types/src/test/java/io/helidon/common/types/SignatureTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +class SignatureTest { + + @Test + void testMethodSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method2") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m4 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .parameterArguments(List.of(TypedElementInfo.builder() + .kind(ElementKind.PARAMETER) + .typeName(TypeNames.STRING) + .elementName("param1") + .buildPrototype())) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + ElementSignature s4 = m4.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("method()")); + assertThat(s2.text(), is("method()")); + assertThat(s3.text(), is("method2()")); + assertThat(s4.text(), is("method(java.lang.String)")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + assertThat(s1, not(s4)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("method")); + assertThat(s1.type(), is(TypeNames.STRING)); + assertThat(s1.parameterTypes(), is(List.of())); + } + + @Test + void testConstructorSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .parameterArguments(List.of(TypedElementInfo.builder() + .kind(ElementKind.PARAMETER) + .typeName(TypeNames.STRING) + .elementName("param1") + .buildPrototype())) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("()")); + assertThat(s2.text(), is("()")); + assertThat(s3.text(), is("(java.lang.String)")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("")); + assertThat(s1.type(), is(TypeNames.PRIMITIVE_VOID)); + assertThat(s1.parameterTypes(), is(List.of())); + } + + @Test + void testFieldSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field2") + .typeName(TypeNames.STRING) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("field")); + assertThat(s2.text(), is("field")); + assertThat(s3.text(), is("field2")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("field")); + assertThat(s1.type(), is(TypeNames.STRING)); + assertThat(s1.parameterTypes(), is(List.of())); + } +} diff --git a/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java b/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java new file mode 100644 index 00000000000..f00f182dc2b --- /dev/null +++ b/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +class TypeInfoTest { + @Test + void testFindInHierarchyInterfaces() { + TypeName ifaceA = TypeName.create("io.helidon.common.types.test.A"); + TypeName ifaceB = TypeName.create("io.helidon.common.types.test.B"); + TypeName ifaceC = TypeName.create("io.helidon.common.types.test.C"); + TypeInfo aInfo = TypeInfo.builder() + .typeName(ifaceA) + .kind(ElementKind.INTERFACE) + .build(); + TypeInfo bInfo = TypeInfo.builder() + .typeName(ifaceB) + .kind(ElementKind.INTERFACE) + .addInterfaceTypeInfo(aInfo) + .build(); + TypeInfo cInfo = TypeInfo.builder() + .typeName(ifaceC) + .kind(ElementKind.INTERFACE) + .addInterfaceTypeInfo(bInfo) + .build(); + + Optional foundInfo = cInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = cInfo.findInHierarchy(ifaceB); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(bInfo)); + + foundInfo = bInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = aInfo.findInHierarchy(ifaceB); + assertThat(foundInfo, is(Optional.empty())); + } + + @Test + void testFindInHierarchyTypes() { + TypeName ifaceA = TypeName.create("io.helidon.common.types.test.A"); + TypeName classB = TypeName.create("io.helidon.common.types.test.B"); + TypeName classC = TypeName.create("io.helidon.common.types.test.C"); + TypeInfo aInfo = TypeInfo.builder() + .typeName(ifaceA) + .kind(ElementKind.INTERFACE) + .build(); + TypeInfo bInfo = TypeInfo.builder() + .typeName(classB) + .kind(ElementKind.CLASS) + .addInterfaceTypeInfo(aInfo) + .build(); + TypeInfo cInfo = TypeInfo.builder() + .typeName(classC) + .kind(ElementKind.INTERFACE) + .superTypeInfo(bInfo) + .build(); + + Optional foundInfo = cInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = cInfo.findInHierarchy(classB); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(bInfo)); + + foundInfo = bInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = aInfo.findInHierarchy(classB); + assertThat(foundInfo, is(Optional.empty())); + } + +} diff --git a/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java b/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java index d6e4490f905..0f09f8bbe47 100644 --- a/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java +++ b/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,26 @@ void testNested() { assertThat(name.classNameWithEnclosingNames(), is("TestType.NestedType.DoubleNestedType")); } + @Test + void testGenericInnerType() { + String resolved = + "io.helidon.service.inject.api.Injection.ScopeHandler"; + + TypeName typeName = TypeName.builder() + .packageName("io.helidon.service.inject.api") + .className("ScopeHandler") + .addEnclosingName("Injection") + .addTypeArgument(TypeName.create("io.helidon.examples.inject.CustomScopeExample.MyScope")) + .build(); + + assertThat(typeName.resolvedName(), is(resolved)); + assertThat(typeName.fqName(), is("io.helidon.service.inject.api.Injection.ScopeHandler")); + + ResolvedType rt = ResolvedType.create(typeName); + assertThat(rt.type().resolvedName(), is(resolved)); + assertThat(rt.type().fqName(), is("io.helidon.service.inject.api.Injection.ScopeHandler")); + } + @Test void testNestedEquality() { TypeName first = create(TestType.NestedType.class); diff --git a/common/uri/pom.xml b/common/uri/pom.xml index 23a1dd61bf3..48eea6dc12a 100644 --- a/common/uri/pom.xml +++ b/common/uri/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-uri Helidon Common URI diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java b/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java index 70a5e1f213e..f7abb5e1ce8 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java @@ -43,19 +43,27 @@ private UriEncoding() { /** * Decode a URI segment. + *

    + * Percent characters {@code "%s"} found between brackets {@code "[]"} are not decoded to support IPv6 literal. + * E.g. {@code http://[fe80::1%lo0]:8080}. + *

    + * See RFC 6874, section 2. * * @param uriSegment URI segment with percent encoding * @return decoded string */ public static String decodeUri(String uriSegment) { - if (uriSegment.isEmpty()) { - return ""; - } - if (uriSegment.indexOf('%') == -1 && uriSegment.indexOf('+') == -1) { - return uriSegment; - } + return decodeUri(uriSegment, true); + } - return decode(uriSegment); + /** + * Decode a URI query. + * + * @param uriQuery URI query with percent encoding + * @return decoded string + */ + public static String decodeQuery(String uriQuery) { + return decodeUri(uriQuery, false); } /** @@ -123,7 +131,18 @@ private static void appendEscape(StringBuilder appender, int b) { appender.append(HEX_DIGITS[b & 0x0F]); } - private static String decode(String string) { + private static String decodeUri(String uriSegment, boolean ignorePercentInBrackets) { + if (uriSegment.isEmpty()) { + return ""; + } + if (uriSegment.indexOf('%') == -1 && uriSegment.indexOf('+') == -1) { + return uriSegment; + } + return decode(uriSegment, ignorePercentInBrackets); + } + + // see java.net.URI.decode(String, boolean) + private static String decode(String string, boolean ignorePercentInBrackets) { int len = string.length(); StringBuilder sb = new StringBuilder(len); @@ -141,7 +160,7 @@ private static String decode(String string) { } else if (betweenBrackets && c == ']') { betweenBrackets = false; } - if (c != '%' || betweenBrackets) { + if (c != '%' || (betweenBrackets && ignorePercentInBrackets)) { sb.append(c == '+' && !betweenBrackets ? ' ' : c); // handles '+' decoding if (++i >= len) { break; diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriFragment.java b/common/uri/src/main/java/io/helidon/common/uri/UriFragment.java index eb44bb1c0c0..11b1f5d19e1 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriFragment.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ private UriFragment(String encoded, String fragment) { * @return a new instance */ public static UriFragment create(String rawFragment) { + Objects.requireNonNull(rawFragment); return new UriFragment(rawFragment); } @@ -104,6 +105,9 @@ public boolean hasValue() { * @return encoded fragment */ public String rawValue() { + if (rawFragment == null) { + throw new IllegalStateException("UriFragment does not have a value, guard with hasValue()"); + } return rawFragment; } @@ -114,7 +118,7 @@ public String rawValue() { */ public String value() { if (decodedFragment == null) { - decodedFragment = UriEncoding.decodeUri(rawFragment); + decodedFragment = UriEncoding.decodeUri(rawValue()); } return decodedFragment; } diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQuery.java b/common/uri/src/main/java/io/helidon/common/uri/UriQuery.java index 2b41d70578a..f1e24fa43a2 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQuery.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQuery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,17 +31,33 @@ public interface UriQuery extends Parameters { /** * Create a new HTTP query from the query string. + * This method does not validate the raw query against specification. * * @param query raw query string * @return HTTP query instance + * @see #create(String, boolean) */ static UriQuery create(String query) { + return create(query, false); + } + + /** + * Create a new HTTP query from the query string, validating if requested. + * + * @param query raw query string + * @param validate whether to validate that the query is according to the specification + * @return HTTP query instance + */ + static UriQuery create(String query, boolean validate) { Objects.requireNonNull(query, "Raw query string cannot be null, use create(URI) or empty()"); if (query.isEmpty()) { return empty(); } + if (validate) { + return new UriQueryImpl(query).validate(); + } return new UriQueryImpl(query); } diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java index dbe14f22650..cba7853848b 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java @@ -31,7 +31,7 @@ import io.helidon.common.mapper.OptionalValue; import io.helidon.common.mapper.Value; -import static io.helidon.common.uri.UriEncoding.decodeUri; +import static io.helidon.common.uri.UriEncoding.decodeQuery; // must be lazily populated to prevent perf overhead when queries are ignored final class UriQueryImpl implements UriQuery { @@ -190,6 +190,11 @@ public String toString() { return "?" + rawValue(); } + UriQuery validate() { + UriValidator.validateQuery(query); + return this; + } + private void ensureDecoded() { if (decodedQueryParams == null) { Map> newQueryParams = new HashMap<>(); @@ -215,11 +220,11 @@ private void ensureDecoded() { private void addDecoded(Map> newQueryParams, String next) { int eq = next.indexOf('='); if (eq == -1) { - newQueryParams.putIfAbsent(decodeUri(next), new LinkedList<>()); + newQueryParams.putIfAbsent(decodeQuery(next), new LinkedList<>()); } else { String name = next.substring(0, eq); String value = next.substring(eq + 1); - newQueryParams.computeIfAbsent(decodeUri(name), it -> new LinkedList<>()).add(decodeUri(value)); + newQueryParams.computeIfAbsent(decodeQuery(name), it -> new LinkedList<>()).add(decodeQuery(value)); } } diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java index c8318896314..12310b8749f 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,8 +85,11 @@ static UriQueryWriteable create() { UriQueryWriteable remove(String name, Consumer> removedConsumer); /** - * Update from a query string (with decoded values). - * @param queryString decoded query string to update this instance + * Update from a query string (with encoded values). + *

    + * This documentation (and behavior) has been changed, as we cannot create a proper query from {@code decoded} values, + * as these may contain characters used to split the query. + * @param queryString encoded query string to update this instance */ void fromQueryString(String queryString); diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java index 2dc04a51670..b6c2ba45aa2 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java @@ -292,11 +292,21 @@ public String toString() { private void addRaw(String next) { int eq = next.indexOf('='); if (eq == -1) { - set(next); + addRaw(next, ""); } else { String name = next.substring(0, eq); String value = next.substring(eq + 1); - set(name, value); + addRaw(name, value); } } + + private void addRaw(String encodedName, String encodedValue) { + String decodedName = UriEncoding.decodeUri(encodedName); + String decodedValue = UriEncoding.decodeUri(encodedValue); + + rawQueryParams.computeIfAbsent(encodedName, it -> new ArrayList<>(1)) + .add(encodedValue); + decodedQueryParams.computeIfAbsent(decodedName, it -> new ArrayList<>(1)) + .add(decodedValue); + } } diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriValidationException.java b/common/uri/src/main/java/io/helidon/common/uri/UriValidationException.java new file mode 100644 index 00000000000..8393d91297d --- /dev/null +++ b/common/uri/src/main/java/io/helidon/common/uri/UriValidationException.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.uri; + +import java.util.Objects; + +import static io.helidon.common.uri.UriValidator.encode; +import static io.helidon.common.uri.UriValidator.print; + +/** + * A URI validation exception. + *

    + * This type provides access to the invalid value that is not cleaned, through {@link #invalidValue()}. + * The exception message is cleaned and can be logged and returned to users ({@link #getMessage()}). + * + * @see #invalidValue() + */ +public class UriValidationException extends IllegalArgumentException { + /** + * Segment that failed validation. + */ + private final Segment segment; + /** + * The value (containing illegal characters) that failed validation. + */ + private final char[] invalidValue; + + /** + * Create a new validation exception that uses a descriptive message and the failed chars. + * The message provided will be appended with cleaned invalid value in double quotes. + * + * @param segment segment that caused this exception + * @param invalidValue value that failed validation + * @param message descriptive message + */ + public UriValidationException(Segment segment, char[] invalidValue, String message) { + super(toMessage(invalidValue, message)); + + this.segment = segment; + this.invalidValue = invalidValue; + } + + /** + * Create a new validation exception that uses a descriptive message and the failed chars. + * + * @param segment segment that caused this exception + * @param invalidValue value that failed validation + * @param validated a validated section of the full value + * @param message descriptive message + */ + UriValidationException(Segment segment, char[] invalidValue, char[] validated, String message) { + super(toMessage(invalidValue, validated, message)); + + this.segment = segment; + this.invalidValue = invalidValue; + } + + /** + * Create a new validation exception that uses a descriptive message and the failed chars. + * + * @param segment segment that caused this exception + * @param invalidValue value that failed validation + * @param validated a validated section of the full value + * @param message descriptive message + * @param index index in the {@code validated} array that failed + * @param c character that was invalid + */ + UriValidationException(Segment segment, char[] invalidValue, char[] validated, String message, int index, char c) { + super(toMessage(invalidValue, validated, message, index, c)); + + this.segment = segment; + this.invalidValue = invalidValue; + } + + /** + * Create a new validation exception that uses a descriptive message and the failed chars. + * + * @param segment segment that caused this exception + * @param invalidValue value that failed validation + * @param message descriptive message + * @param index index in the {@code invalidValue} array that failed + * @param c character that was invalid + */ + UriValidationException(Segment segment, char[] invalidValue, String message, int index, char c) { + super(toMessage(invalidValue, message, index, c)); + + this.segment = segment; + this.invalidValue = invalidValue; + } + + /** + * The value that did not pass validation. + * This value is as it was received over the network, so it is not safe to log or return to the user! + * + * @return invalid value that failed validation + */ + public char[] invalidValue() { + return invalidValue; + } + + /** + * Segment that caused this validation exception. + * + * @return segment of the URI + */ + public Segment segment() { + return segment; + } + + private static String toMessage(char[] value, String message) { + Objects.requireNonNull(value); + Objects.requireNonNull(message); + + if (value.length == 0) { + return message; + } + return message + ": " + encode(value); + } + + private static String toMessage(char[] value, char[] validated, String message) { + Objects.requireNonNull(value); + Objects.requireNonNull(message); + Objects.requireNonNull(validated); + + if (validated.length == 0) { + if (value.length == 0) { + return message; + } + return message + ". Value: " + encode(value); + } + if (value.length == 0) { + return message + ": " + encode(validated); + } + return message + ": " + encode(validated) + + ". Value: " + encode(value); + } + + private static String toMessage(char[] value, char[] validated, String message, int index, char c) { + Objects.requireNonNull(value); + Objects.requireNonNull(validated); + Objects.requireNonNull(message); + + return message + ": " + encode(validated) + ", index: " + index + + ", char: " + print(c) + + ". Value: " + encode(value); + } + + private static String toMessage(char[] value, String message, int index, char c) { + Objects.requireNonNull(value); + Objects.requireNonNull(message); + + return message + ": " + encode(value) + ", index: " + index + + ", char: " + print(c); + } + + /** + * Segment of the URI that caused this validation failure. + */ + public enum Segment { + /** + * URI Scheme. + */ + SCHEME("Scheme"), + /** + * URI Host. + */ + HOST("Host"), + /** + * URI Path. + */ + PATH("Path"), + /** + * URI Query. + */ + QUERY("Query"), + /** + * URI Fragment. + */ + FRAGMENT("Fragment"); + private final String name; + + Segment(String name) { + this.name = name; + } + + /** + * Human-readable text that describes this segment. + * + * @return segment text + */ + public String text() { + return name; + } + } +} diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriValidator.java b/common/uri/src/main/java/io/helidon/common/uri/UriValidator.java new file mode 100644 index 00000000000..c2032a8ba67 --- /dev/null +++ b/common/uri/src/main/java/io/helidon/common/uri/UriValidator.java @@ -0,0 +1,653 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.uri; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.helidon.common.uri.UriValidationException.Segment; + +/** + * Validate parts of the URI. + *

    + * Validation is based on + * RFC-3986. + *

    + * The following list provides an overview of parts of URI and how/if we validate it: + *

      + *
    • scheme - {@link #validateScheme(String)}
    • + *
    • authority - {@link #validateHost(String)}, port is validated in HTTP processing
    • + *
    • path - see {@link io.helidon.common.uri.UriPath#validate()}
    • + *
    • query - {@link #validateQuery(String)}
    • + *
    • fragment - {@link #validateFragment(String)}
    • + *
    + */ +public final class UriValidator { + private static final Pattern IP_V4_PATTERN = + Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$"); + private static final boolean[] HEXDIGIT = new boolean[256]; + private static final boolean[] UNRESERVED = new boolean[256]; + private static final boolean[] SUB_DELIMS = new boolean[256]; + // characters (in addition to hex, unreserved and sub-delims) that can be safely printed + private static final boolean[] PRINTABLE = new boolean[256]; + + static { + // digits + for (int i = '0'; i <= '9'; i++) { + UNRESERVED[i] = true; + } + // alpha + for (int i = 'a'; i <= 'z'; i++) { + UNRESERVED[i] = true; + } + for (int i = 'A'; i <= 'Z'; i++) { + UNRESERVED[i] = true; + } + UNRESERVED['-'] = true; + UNRESERVED['.'] = true; + UNRESERVED['_'] = true; + UNRESERVED['~'] = true; + + // hexdigits + // digits + for (int i = '0'; i <= '9'; i++) { + HEXDIGIT[i] = true; + } + // alpha + for (int i = 'a'; i <= 'f'; i++) { + HEXDIGIT[i] = true; + } + for (int i = 'A'; i <= 'F'; i++) { + HEXDIGIT[i] = true; + } + + // sub-delim set + SUB_DELIMS['!'] = true; + SUB_DELIMS['$'] = true; + SUB_DELIMS['&'] = true; + SUB_DELIMS['\''] = true; + SUB_DELIMS['('] = true; + SUB_DELIMS[')'] = true; + SUB_DELIMS['*'] = true; + SUB_DELIMS['+'] = true; + SUB_DELIMS[','] = true; + SUB_DELIMS[';'] = true; + SUB_DELIMS['='] = true; + + PRINTABLE[':'] = true; + PRINTABLE['/'] = true; + PRINTABLE['?'] = true; + PRINTABLE['@'] = true; + PRINTABLE['%'] = true; + PRINTABLE['#'] = true; + PRINTABLE['['] = true; + PRINTABLE[']'] = true; + } + + private UriValidator() { + } + + /** + * Validate a URI scheme. + * + * @param scheme scheme to validate + * @throws io.helidon.common.uri.UriValidationException in case there are invalid characters in the scheme + */ + public static void validateScheme(String scheme) { + if ("http".equals(scheme)) { + return; + } + if ("https".equals(scheme)) { + return; + } + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + char[] chars = scheme.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + validateAscii(Segment.SCHEME, chars, i, c); + if (Character.isLetterOrDigit(c)) { + continue; + } + if (c == '+') { + continue; + } + if (c == '-') { + continue; + } + if (c == '.') { + continue; + } + failInvalidChar(Segment.SCHEME, chars, i, c); + } + } + + /** + * Validate a URI Query raw string. + * + * @param rawQuery query to validate + * @throws io.helidon.common.uri.UriValidationException in case there are invalid characters in the query + */ + public static void validateQuery(String rawQuery) { + Objects.requireNonNull(rawQuery); + + // empty query is valid + if (rawQuery.isEmpty()) { + return; + } + + // query = *( pchar / "/" / "?" ) + // pchar = unreserved / pct-encoded / sub-delims / "@" + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + // pct-encoded = "%" HEXDIG HEXDIG + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + + char[] chars = rawQuery.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + validateAscii(Segment.QUERY, chars, i, c); + if (UNRESERVED[c]) { + continue; + } + if (SUB_DELIMS[c]) { + continue; + } + if (c == '@') { + continue; + } + if (c == '/') { + continue; + } + if (c == '?') { + continue; + } + // done with pchar validation except for percent encoded + if (c == '%') { + // percent encoding + validatePercentEncoding(Segment.QUERY, rawQuery, chars, i); + i += 2; + continue; + } + failInvalidChar(Segment.QUERY, chars, i, c); + } + } + + /** + * Validate a host string. + * + * @param host host to validate + * @throws io.helidon.common.uri.UriValidationException in case there are invalid characters in the host + */ + public static void validateHost(String host) { + Objects.requireNonNull(host); + if (host.indexOf('[') == 0 && host.indexOf(']') == host.length() - 1) { + validateIpLiteral(host); + } else { + validateNonIpLiteral(host); + } + } + + /** + * An IP literal starts with {@code [} and ends with {@code ]}. + * + * @param ipLiteral host literal string, may be an IPv6 address, or IP version future + * @throws io.helidon.common.uri.UriValidationException in case there are invalid characters in the host + */ + public static void validateIpLiteral(String ipLiteral) { + Objects.requireNonNull(ipLiteral); + checkNotBlank(Segment.HOST, "IP Literal", ipLiteral, ipLiteral); + + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" + if (ipLiteral.charAt(0) != '[') { + throw new UriValidationException(Segment.HOST, + ipLiteral.toCharArray(), + "Invalid IP literal, missing square bracket(s)", + 0, + ipLiteral.charAt(0)); + } + int lastIndex = ipLiteral.length() - 1; + if (ipLiteral.charAt(lastIndex) != ']') { + throw new UriValidationException(Segment.HOST, + ipLiteral.toCharArray(), + "Invalid IP literal, missing square bracket(s)", + lastIndex, + ipLiteral.charAt(lastIndex)); + } + + String host = ipLiteral.substring(1, ipLiteral.length() - 1); + checkNotBlank(Segment.HOST, "Host", ipLiteral, host); + if (host.charAt(0) == 'v') { + // IP future - starts with version `v1` etc. + validateIpFuture(ipLiteral, host); + return; + } + // IPv6 + /* + IPv6address = 6( h16 ":" ) ls32 + / "::" 5( h16 ":" ) ls32 + / [ h16 ] "::" 4( h16 ":" ) ls32 + / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + / [ *4( h16 ":" ) h16 ] "::" ls32 + / [ *5( h16 ":" ) h16 ] "::" h16 + / [ *6( h16 ":" ) h16 ] "::" + + ls32 = ( h16 ":" h16 ) / IPv4address + h16 = 1*4HEXDIG + */ + if (host.equals("::")) { + // all empty + return; + } + if (host.equals("::1")) { + // localhost + return; + } + boolean skipped = false; + int segments = 0; // max segments is 8 (full IPv6 address) + String inProgress = host; + while (!inProgress.isEmpty()) { + if (inProgress.length() == 1) { + segments++; + validateH16(ipLiteral, inProgress); + break; + } + if (inProgress.charAt(0) == ':' && inProgress.charAt(1) == ':') { + // :: means skip everything that was before (or everything that is after) + if (skipped) { + throw new UriValidationException(Segment.HOST, + ipLiteral.toCharArray(), + "Host IPv6 contains more than one skipped segment"); + } + skipped = true; + segments++; + inProgress = inProgress.substring(2); + continue; + } + if (inProgress.charAt(0) == ':') { + throw new UriValidationException(Segment.HOST, + ipLiteral.toCharArray(), + inProgress.toCharArray(), + "Host IPv6 contains excessive colon"); + } + // this must be h16 (or an IPv4 address) + int nextColon = inProgress.indexOf(':'); + if (nextColon == -1) { + // the rest of the string + if (inProgress.indexOf('.') == -1) { + segments++; + validateH16(ipLiteral, inProgress); + } else { + Matcher matcher = IP_V4_PATTERN.matcher(inProgress); + if (matcher.matches()) { + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address", ipLiteral, matcher.group(1)); + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address", ipLiteral, matcher.group(2)); + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address", ipLiteral, matcher.group(3)); + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address", ipLiteral, matcher.group(4)); + } else { + throw new UriValidationException(Segment.HOST, + ipLiteral.toCharArray(), + "Host IPv6 dual address contains invalid IPv4 address"); + } + } + break; + } + validateH16(ipLiteral, inProgress.substring(0, nextColon)); + segments++; + if (inProgress.length() >= nextColon + 2) { + if (inProgress.charAt(nextColon + 1) == ':') { + // double colon, keep it there + inProgress = inProgress.substring(nextColon); + continue; + } + } + inProgress = inProgress.substring(nextColon + 1); + if (inProgress.isBlank()) { + // this must fail on empty segment + validateH16(ipLiteral, inProgress); + } + } + + if (segments > 8) { + throw new UriValidationException(Segment.HOST, + ipLiteral.toCharArray(), + "Host IPv6 address contains too many segments"); + } + } + + /** + * Validate IPv4 address or a registered name. + * + * @param host string with either an IPv4 address, or a registered name + * @throws io.helidon.common.uri.UriValidationException in case there are invalid characters in the host + */ + public static void validateNonIpLiteral(String host) { + Objects.requireNonNull(host); + checkNotBlank(Segment.HOST, "Host", host, host); + + // Ipv4 address: 127.0.0.1 + Matcher matcher = IP_V4_PATTERN.matcher(host); + if (matcher.matches()) { + /* + IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + dec-octet = DIGIT ; 0-9 + / %x31-39 DIGIT ; 10-99 + / "1" 2DIGIT ; 100-199 + / "2" %x30-34 DIGIT ; 200-249 + / "25" %x30-35 ; 250-255 + */ + + // we have found an IPv4 address, or a valid registered name (555.555.555.555 is a valid name...) + return; + } + + // everything else is a registered name + + // registered name + /* + reg-name = *( unreserved / pct-encoded / sub-delims ) + pct-encoded = "%" HEXDIG HEXDIG + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" + */ + char[] chars = host.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + validateAscii(Segment.HOST, chars, i, c); + + if (UNRESERVED[c]) { + continue; + } + if (SUB_DELIMS[c]) { + continue; + } + if (c == '%') { + // percent encoding + validatePercentEncoding(Segment.HOST, host, chars, i); + i += 2; + continue; + } + failInvalidChar(Segment.HOST, chars, i, c); + } + } + + /** + * Validate URI fragment. + * + * @param rawFragment fragment to validate + * @throws io.helidon.common.uri.UriValidationException in case there are invalid characters in the fragment + */ + public static void validateFragment(String rawFragment) { + Objects.requireNonNull(rawFragment); + + if (rawFragment.isEmpty()) { + return; + } + char[] chars = rawFragment.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + validateAscii(Segment.FRAGMENT, chars, i, c); + + // *( pchar / "/" / "?" ) + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + + if (UNRESERVED[c]) { + continue; + } + if (SUB_DELIMS[c]) { + continue; + } + if (c == '@') { + continue; + } + if (c == ':') { + continue; + } + + // done with pchar validation except for percent encoded + if (c == '%') { + // percent encoding + validatePercentEncoding(Segment.FRAGMENT, rawFragment, chars, i); + i += 2; + continue; + } + failInvalidChar(Segment.FRAGMENT, chars, i, c); + } + } + + static String print(char c) { + if (printable(c)) { + return "'" + c + "'"; + } + return "0x" + hex(c); + } + + static String encode(char[] chars) { + StringBuilder result = new StringBuilder(chars.length); + + for (char aChar : chars) { + if (aChar > 254) { + result.append('?'); + continue; + } + if (printable(aChar)) { + result.append(aChar); + continue; + } + result.append('?'); + } + + return result.toString(); + } + + private static void failInvalidChar(Segment segment, char[] chars, int i, char c) { + throw new UriValidationException(segment, + chars, + segment.text() + " contains invalid char", + i, + c); + } + + private static void validateAscii(Segment segment, char[] chars, int i, char c) { + if (c > 254) { + // in general only ASCII characters are allowed + throw new UriValidationException(segment, + chars, + segment.text() + " contains invalid char (non-ASCII)", + i, + c); + } + } + + /** + * Validate percent encoding sequence. + * + * @param segment segment of the URI + * @param chars characters of the part + * @param i index of the percent + */ + private static void validatePercentEncoding(Segment segment, String value, char[] chars, int i) { + if (i + 2 >= chars.length) { + throw new UriValidationException(segment, + chars, + segment.text() + + " contains invalid % encoding, not enough chars left at index " + + i); + } + char p1 = chars[i + 1]; + char p2 = chars[i + 2]; + // %p1p2 + validateHex(segment, value, chars, p1, segment.text(), i + 1, true); + validateHex(segment, value, chars, p2, segment.text(), i + 2, true); + } + + private static void validateHex(Segment segment, + String fullValue, + char[] chars, + char c, + String type, + int index, + boolean isPercentEncoding) { + if (c > 255 || !HEXDIGIT[c]) { + if (fullValue.length() == chars.length) { + if (isPercentEncoding) { + throw new UriValidationException(segment, + chars, + type + " has non hexadecimal char in % encoding", + index, + c); + } + throw new UriValidationException(segment, + chars, + type + " has non hexadecimal char", + index, + c); + } else { + if (isPercentEncoding) { + throw new UriValidationException(segment, + fullValue.toCharArray(), + chars, + type + " has non hexadecimal char in % encoding", + index, + c); + } + throw new UriValidationException(segment, + fullValue.toCharArray(), + chars, + type + " has non hexadecimal char", + index, + c); + } + } + } + + private static String hex(char c) { + String hexString = Integer.toHexString(c); + if (hexString.length() == 1) { + return "0" + hexString; + } + return hexString; + } + + private static void validateH16(String host, String inProgress) { + if (inProgress.isBlank()) { + throw new UriValidationException(Segment.HOST, + host.toCharArray(), + "IPv6 segment is empty"); + } + if (inProgress.length() > 4) { + throw new UriValidationException(Segment.HOST, + host.toCharArray(), + inProgress.toCharArray(), + "IPv6 segment has more than 4 chars"); + } + validateHexDigits(Segment.HOST, "IPv6 segment", host, inProgress); + } + + private static void validateHexDigits(Segment segment, + String description, + String host, + String section) { + char[] chars = section.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + validateHex(segment, host, chars, c, description, i, false); + } + } + + private static void validateIpOctet(String message, String host, String octet) { + int octetInt = Integer.parseInt(octet); + // cannot be negative, as the regexp will not match + if (octetInt > 255) { + throw new UriValidationException(Segment.HOST, host.toCharArray(), message); + } + } + + private static void validateIpFuture(String ipLiteral, String host) { + /* + IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + */ + int dot = host.indexOf('.'); + if (dot == -1) { + throw new UriValidationException(Segment.HOST, + ipLiteral.toCharArray(), + "IP Future must contain 'v.'"); + } + // always starts with v + String version = host.substring(1, dot); + checkNotBlank(Segment.HOST, "Version", ipLiteral, version); + validateHexDigits(Segment.HOST, "Future version", ipLiteral, version); + + String address = host.substring(dot + 1); + checkNotBlank(Segment.HOST, "IP Future", ipLiteral, address); + + char[] chars = address.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + validateAscii(Segment.HOST, chars, i, c); + if (UNRESERVED[c]) { + continue; + } + if (SUB_DELIMS[c]) { + continue; + } + if (c == ':') { + continue; + } + failInvalidChar(Segment.HOST, ipLiteral.toCharArray(), i + dot + 1, c); + } + } + + private static void checkNotBlank(Segment segment, + String message, + String ipLiteral, + String toValidate) { + if (toValidate.isBlank()) { + if (ipLiteral.equals(toValidate)) { + throw new UriValidationException(segment, ipLiteral.toCharArray(), message + " cannot be blank"); + } else { + throw new UriValidationException(segment, + ipLiteral.toCharArray(), + toValidate.toCharArray(), + message + " cannot be blank"); + } + } + } + + private static boolean printable(char c) { + if (c > 254) { + return false; + } + if (UNRESERVED[c]) { + return true; + } + if (SUB_DELIMS[c]) { + return true; + } + if (PRINTABLE[c]) { + return true; + } + return false; + } +} diff --git a/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java b/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java index 56d791e35d2..e8c1214d979 100644 --- a/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java +++ b/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java @@ -30,4 +30,9 @@ void testSpaceDecoding() { assertThat(decodeUri("+hello+world+"), is(" hello world ")); assertThat(decodeUri("[+]hello[+]world[+]"), is("[+]hello[+]world[+]")); } + + @Test + void testIPv6Literal() { + assertThat(decodeUri("http://[fe80::1%lo0]:8080"), is("http://[fe80::1%lo0]:8080")); + } } diff --git a/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java b/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java index edb01dd62bd..3c011ac81d0 100644 --- a/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java +++ b/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java @@ -16,7 +16,6 @@ package io.helidon.common.uri; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; @@ -41,11 +40,17 @@ void sanityParse() { } @Test - void testEncoded() throws UnsupportedEncodingException { + void testEncoded() { UriQuery uriQuery = UriQuery.create("a=" + URLEncoder.encode("1&b=2", US_ASCII)); assertThat(uriQuery.get("a"), is("1&b=2")); } + @Test + void testEncodedWithinBrackets() { + UriQuery uriQuery = UriQuery.create("msg=[Hello%20World]"); + assertThat(uriQuery.get("msg"), is("[Hello World]")); + } + @Test void testEncodedOtherChars() { UriQuery uriQuery = UriQuery.create("a=b%26c=d&e=f&e=g&h=x%63%23e%3c"); @@ -57,7 +62,7 @@ void testEncodedOtherChars() { assertThat(uriQuery.get("h"), is("xc#e<")); assertThat(uriQuery.get("a"), is("b&c=d")); } - + @Test void testEmptyQueryString() { UriQuery uriQuery = UriQuery.create(""); @@ -80,10 +85,22 @@ void issue8710() { UriQuery uriQuery = UriQuery.create(URI.create("http://foo/bar?a&b=c")); OptionalValue optional = uriQuery.first("a"); assertThat(optional.isEmpty(), is(true)); - + assertThat(uriQuery.all("a"), hasItems()); assertThat(uriQuery.all("b"), hasItems("c")); assertThat(uriQuery.getRaw("a"), is("")); } + @Test + void testFromQueryString() { + UriQueryWriteable query = UriQueryWriteable.create(); + query.fromQueryString("p1=v1&p2=v2&p3=%2F%2Fv3%2F%2F&p4=a%20b%20c"); + assertThat(query.get("p1"), is("v1")); + assertThat(query.get("p2"), is("v2")); + assertThat(query.get("p3"), is("//v3//")); + // make sure the encoded value is correct + assertThat(query.getRaw("p3"), is("%2F%2Fv3%2F%2F")); + assertThat(query.get("p4"), is("a b c")); + assertThat(query.getRaw("p4"), is("a%20b%20c")); + } } \ No newline at end of file diff --git a/common/uri/src/test/java/io/helidon/common/uri/UriValidatorTest.java b/common/uri/src/test/java/io/helidon/common/uri/UriValidatorTest.java new file mode 100644 index 00000000000..659e5a98249 --- /dev/null +++ b/common/uri/src/test/java/io/helidon/common/uri/UriValidatorTest.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.uri; + +import java.net.URI; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.uri.UriValidator.validateFragment; +import static io.helidon.common.uri.UriValidator.validateHost; +import static io.helidon.common.uri.UriValidator.validateIpLiteral; +import static io.helidon.common.uri.UriValidator.validateScheme; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class UriValidatorTest { + @Test + void testSchemeValidation() { + validateScheme("http"); + validateScheme("https"); + validateScheme("ws"); + validateScheme("abc123+-."); + + assertThrows(NullPointerException.class, () -> UriValidator.validateScheme(null)); + + validateBadScheme("čttp", + "Scheme contains invalid char (non-ASCII): ?ttp, index: 0, char: 0x10d"); + validateBadScheme("h_ttp", + "Scheme contains invalid char: h_ttp, index: 1, char: '_'"); + validateBadScheme("h~ttp", + "Scheme contains invalid char: h~ttp, index: 1, char: '~'"); + validateBadScheme("h!ttp", + "Scheme contains invalid char: h!ttp, index: 1, char: '!'"); + } + + @Test + void testFragmentValidation() { + assertThrows(NullPointerException.class, () -> validateFragment(null)); + validateFragment(""); + validateFragment("fragment"); + validateFragment("frag_ment"); // unreserved + validateFragment("frag~ment"); // unreserved + validateFragment("frag=ment"); // sub-delim + validateFragment("frag=!"); // sub-delim + validateFragment("frag%61ment"); // pct-encoded + validateFragment("frag@ment"); // at sign + validateFragment("frag:ment"); // colon + + validateBadFragment("fragčment", + "Fragment contains invalid char (non-ASCII): frag?ment, index: 4, char: 0x10d"); + validateBadFragment("frag%6147%4", + "Fragment contains invalid % encoding, not enough chars left at index 9: frag%6147%4"); + // percent encoded: first char is invalid + validateBadFragment("frag%6147%X1", + "Fragment has non hexadecimal char in % encoding: frag%6147%X1, index: 10, char: 'X'"); + validateBadFragment("frag%6147%č1", + "Fragment has non hexadecimal char in % encoding: frag%6147%?1, index: 10, char: 0x10d"); + // percent encoded: second char is invalid + validateBadFragment("frag%6147%1X", + "Fragment has non hexadecimal char in % encoding: frag%6147%1X, index: 11, char: 'X'"); + validateBadFragment("frag%6147%1č", + "Fragment has non hexadecimal char in % encoding: frag%6147%1?, index: 11, char: 0x10d"); + // character not in allowed sets + validateBadFragment("frag%6147{", + "Fragment contains invalid char: frag%6147?, index: 9, char: 0x7b"); + validateBadFragment("frag%6147\t", + "Fragment contains invalid char: frag%6147?, index: 9, char: 0x09"); + } + + @Test + void testQueryValidation() { + assertThrows(NullPointerException.class, () -> UriQuery.create((String) null)); + assertThrows(NullPointerException.class, () -> UriQuery.create((URI) null)); + assertThrows(NullPointerException.class, () -> UriQuery.create(null, true)); + assertThrows(NullPointerException.class, () -> UriValidator.validateQuery(null)); + + UriQuery.create("", true); + UriValidator.validateQuery(""); + UriQuery.create("a=b&c=d&a=e", true); + // validate all rules + // must be an ASCII (lower than 255) + validateBadQuery("a=@/?%6147č", + "Query contains invalid char (non-ASCII): a=@/?%6147?, index: 10, char: 0x10d"); + // percent encoded: must be full percent encoding + validateBadQuery("a=@/?%6147%4", + "Query contains invalid % encoding, not enough chars left at index 10: a=@/?%6147%4"); + // percent encoded: first char is invalid + validateBadQuery("a=@/?%6147%X1", + "Query has non hexadecimal char in % encoding: a=@/?%6147%X1, index: 11, char: 'X'"); + validateBadQuery("a=@/?%6147%č1", + "Query has non hexadecimal char in % encoding: a=@/?%6147%?1, index: 11, char: 0x10d"); + // percent encoded: second char is invalid + validateBadQuery("a=@/?%6147%1X", + "Query has non hexadecimal char in % encoding: a=@/?%6147%1X, index: 12, char: 'X'"); + validateBadQuery("a=@/?%6147%1č", + "Query has non hexadecimal char in % encoding: a=@/?%6147%1?, index: 12, char: 0x10d"); + // character not in allowed sets + validateBadQuery("a=@/?%6147{", + "Query contains invalid char: a=@/?%6147?, index: 10, char: 0x7b"); + validateBadQuery("a=@/?%6147\t", + "Query contains invalid char: a=@/?%6147?, index: 10, char: 0x09"); + } + + @Test + void testGoodHostname() { + // sanity + validateHost("localhost"); + // host names + validateHost("www.example.com"); + // percent encoded + validateHost("%65%78%61%6D%70%6C%65"); + validateHost("%65%78%61%6D%70%6C%65.com"); + // with underscores + validateHost("www.exa_mple.com"); + // with sub-delims + validateHost("www.exa$mple.com"); + } + + @Test + void testGoodIp4() { + // IPv4 + validateHost("192.167.1.1"); + } + + @Test + void testGoodIpLiteral6() { + // IPv6 + validateHost("[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]"); + validateHost("[::1]"); + validateHost("[2001:db8:3333:4444:5555:6666:7777:8888]"); + validateHost("[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]"); + validateHost("[::]"); + validateHost("[2001:db8::]"); + validateHost("[::1234:5678]"); + validateHost("[::1234:5678:1]"); + validateHost("[2001:db8::1234:5678]"); + validateHost("[2001:db8:1::ab9:C0A8:102]"); + } + + @Test + void testGoodIpLiteral6Dual() { + // IPv6 + validateHost("[2001:db8:3333:4444:5555:6666:1.2.3.4]"); + validateHost("[::11.22.33.44]"); + validateHost("[2001:db8::123.123.123.123]"); + validateHost("[::1234:5678:91.123.4.56]"); + validateHost("[::1234:5678:1.2.3.4]"); + validateHost("[2001:db8::1234:5678:5.6.7.8]"); + } + + @Test + void testGoodIpLiteralFuture() { + // IPvFuture + validateHost("[v9.abc:def]"); + validateHost("[v9.abc:def*]"); + } + + @Test + void testBadHosts() { + // just empty + invokeExpectFailure("Host cannot be blank", ""); + // invalid brackets + invokeExpectFailure("Host contains invalid char: [start.but.not.end, index: 0, char: '['", + "[start.but.not.end"); + invokeExpectFailure("Host contains invalid char: end.but.not.start], index: 17, char: ']'", + "end.but.not.start]"); + invokeExpectFailure("Host contains invalid char: int.the[.middle], index: 7, char: '['", + "int.the[.middle]"); + // invalid escape + invokeExpectFailure("Host has non hexadecimal char in % encoding: www.%ZAxample.com, index: 5, char: 'Z'", + "www.%ZAxample.com"); + invokeExpectFailure("Host has non hexadecimal char in % encoding: www.%AZxample.com, index: 6, char: 'Z'", + "www.%AZxample.com"); + // invalid character (non-ASCII + invokeExpectFailure("Host contains invalid char (non-ASCII): www.?example.com, index: 4, char: 0x10d", + "www.čexample.com"); + // wrong trailing escape (must be two chars); + invokeExpectFailure("Host contains invalid % encoding, not enough chars left at index 15: www.example.com%4", + "www.example.com%4"); + invokeExpectFailure("Host has non hexadecimal char in % encoding: www.example.com%?4, index: 16, char: 0x10d", + "www.example.com%č4"); + invokeExpectFailure("Host has non hexadecimal char in % encoding: www.example.com%4?, index: 17, char: 0x10d", + "www.example.com%4č"); + } + + @Test + void testBadLiteral6() { + // IPv6 + // empty segment + invokeExpectFailure("Host IPv6 contains more than one skipped segment: [2001:db8::85a3::7334]", + "[2001:db8::85a3::7334]"); + // wrong segment (G is not a hexadecimal number) + invokeExpectFailure("IPv6 segment has non hexadecimal char: GGGG, index: 0, char: 'G'. " + + "Value: [GGGG:FFFF:0000:0000:0000:0000:0000:0000]", + "[GGGG:FFFF:0000:0000:0000:0000:0000:0000]"); + // non-ASCII character + invokeExpectFailure("IPv6 segment has non hexadecimal char: ?, index: 0, char: 0x10d. " + + "Value: [?:FFFF:0000:0000:0000:0000:0000:0000]", + "[č:FFFF:0000:0000:0000:0000:0000:0000]"); + // wrong segment (too many characters) + invokeExpectFailure("IPv6 segment has more than 4 chars: aaaaa. " + + "Value: [aaaaa:FFFF:0000:0000:0000:0000:0000:0000]", + "[aaaaa:FFFF:0000:0000:0000:0000:0000:0000]"); + // empty segment + invokeExpectFailure("IPv6 segment is empty: [aaaa:FFFF:0000:0000:0000:0000:0000:]", + "[aaaa:FFFF:0000:0000:0000:0000:0000:]"); + // wrong number of segments + invokeExpectFailure("Host IPv6 address contains too many segments: " + + "[0000:0000:0000:0000:0000:0000:0000:0000:0000:0000]", + "[0000:0000:0000:0000:0000:0000:0000:0000:0000:0000]"); + // missing everything + invokeExpectFailure("Host cannot be blank. Value: []", + "[]"); + // wrong start (leading colon) + invokeExpectFailure("Host IPv6 contains excessive colon: :1:0::. Value: [:1:0::]", + "[:1:0::]"); + // wrong end, colon instead of value + invokeExpectFailure("IPv6 segment has non hexadecimal char: :, index: 0, char: ':'. Value: [1:0:::]", + "[1:0:::]"); + + invokeLiteralExpectFailure("Invalid IP literal, missing square bracket(s): [::, index: 2, char: ':'", + "[::"); + invokeLiteralExpectFailure("Invalid IP literal, missing square bracket(s): ::], index: 0, char: ':'", + "::]"); + } + + @Test + void testBadLiteralDual() { + invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.266.44.74]", + "[::14.266.44.74]"); + invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.266.44]", + "[::14.266.44]"); + invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.123.-44.147]", + "[::14.123.-44.147]"); + } + + @Test + void testBadLiteralFuture() { + // IPv future + // version must be present + invokeExpectFailure("Version cannot be blank. Value: [v.abc:def]", + "[v.abc:def]"); + // missing address + invokeExpectFailure("IP Future must contain 'v.': [v2]", + "[v2]"); + invokeExpectFailure("IP Future cannot be blank. Value: [v2.]", + "[v2.]"); + // invalid character in the host (valid future) + invokeExpectFailure("Host contains invalid char: [v2./0:::], index: 3, char: '/'", + "[v2./0:::]"); + invokeExpectFailure("Host contains invalid char (non-ASCII): 0:?, index: 2, char: 0x10d", + "[v2.0:č]"); + } + + private static void validateBadQuery(String query, String expected) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> UriQuery.create(query, true)); + assertThat(exception.getMessage(), is(expected)); + } + + private static void validateBadScheme(String scheme, String expected) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> validateScheme(scheme)); + assertThat(exception.getMessage(), is(expected)); + } + + private static void validateBadFragment(String fragment, String expected) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> validateFragment(fragment)); + assertThat(exception.getMessage(), is(expected)); + } + + private static void invokeExpectFailure(String message, String host) { + var t = assertThrows(IllegalArgumentException.class, () -> validateHost(host), "Testing host: " + host); + assertThat(t.getMessage(), is(message)); + } + + private static void invokeLiteralExpectFailure(String message, String host) { + var t = assertThrows(IllegalArgumentException.class, () -> validateIpLiteral(host), "Testing host: " + host); + assertThat(t.getMessage(), is(message)); + } +} \ No newline at end of file diff --git a/config/config-mp/pom.xml b/config/config-mp/pom.xml index 6eb598e3c5a..a183efb428e 100644 --- a/config/config-mp/pom.xml +++ b/config/config-mp/pom.xml @@ -22,7 +22,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-mp @@ -79,16 +79,36 @@ - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen ${helidon.version} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java index 8299a1c1cc5..d35fdb91b16 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java @@ -30,6 +30,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; @@ -452,7 +453,11 @@ private Optional> findImplicit(Class type) { if (Enum.class.isAssignableFrom(type)) { return Optional.of(value -> { Class enumClass = (Class) type; - return (T) Enum.valueOf(enumClass, value); + try { + return (T) Enum.valueOf(enumClass, value); + } catch (Exception e) { + return (T) Enum.valueOf(enumClass, value.toUpperCase(Locale.ROOT)); + } }); } // any class that has a "public static T method()" diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java index 9ecd670227d..4b6f57efdc1 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import java.util.stream.Stream; import io.helidon.common.GenericType; +import io.helidon.common.config.GlobalConfig; import io.helidon.config.ConfigValue; import io.helidon.config.MetaConfig; import io.helidon.config.spi.ConfigMapper; @@ -167,8 +168,8 @@ public static void buildTimeEnd() { private ConfigDelegate doRegisterConfig(Config config, ClassLoader classLoader) { ConfigDelegate currentConfig = CONFIGS.remove(classLoader); - if (config instanceof ConfigDelegate) { - config = ((ConfigDelegate) config).delegate(); + if (config instanceof ConfigDelegate delegate) { + config = delegate.delegate(); } if (null != currentConfig) { @@ -178,6 +179,11 @@ private ConfigDelegate doRegisterConfig(Config config, ClassLoader classLoader) ConfigDelegate newConfig = new ConfigDelegate(config); CONFIGS.put(classLoader, newConfig); + if (classLoader == Thread.currentThread().getContextClassLoader()) { + // this should be the default class loader (we do not support classloader magic in Helidon) + GlobalConfig.config(() -> newConfig, true); + } + return newConfig; } diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java b/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java index 514830dab44..5ec1c8d3e43 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,8 +84,8 @@ class SeConfig implements Config { this.stringKey = prefix.child(key).toString(); this.stringPrefix = prefix.toString(); - if (delegate instanceof MpConfigImpl) { - this.delegateImpl = (MpConfigImpl) delegate; + if (delegate instanceof MpConfigImpl mpConfig) { + this.delegateImpl = mpConfig; } else { this.delegateImpl = null; } diff --git a/config/config/pom.xml b/config/config/pom.xml index 374b0294508..256ca780600 100644 --- a/config/config/pom.xml +++ b/config/config/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config Helidon Config diff --git a/config/config/src/main/java/io/helidon/config/BuilderImpl.java b/config/config/src/main/java/io/helidon/config/BuilderImpl.java index 28e60577289..a1bd6f09681 100644 --- a/config/config/src/main/java/io/helidon/config/BuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/BuilderImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023 Oracle and/or its affiliates. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,6 +61,7 @@ class BuilderImpl implements Config.Builder { private MergingStrategy mergingStrategy = MergingStrategy.fallback(); private boolean hasSystemPropertiesSource; private boolean hasEnvVarSource; + private boolean sourcesConfigured; /* * Config mapper providers */ @@ -123,6 +124,8 @@ public Config.Builder sources(List> sourceSuppl sourceSuppliers.stream() .map(Supplier::get) .forEach(this::addSource); + // this was intentional, even if empty (such as from Config.just()) + this.sourcesConfigured = true; return this; } @@ -427,14 +430,14 @@ private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) { envVarAliasGeneratorEnabled = true; } - boolean nothingConfigured = sources.isEmpty(); + boolean nothingConfigured = sources.isEmpty() && !sourcesConfigured; if (nothingConfigured) { // use meta configuration to load all sources - MetaConfig.configSources(mediaType -> context.findParser(mediaType).isPresent(), context.supportedSuffixes()) - .stream() + MetaConfigFinder.findConfigSource(mediaType -> context.findParser(mediaType).isPresent(), + context.supportedSuffixes()) .map(context::sourceRuntimeBase) - .forEach(targetSources::add); + .ifPresent(targetSources::add); } else { // add all configured or discovered sources @@ -702,56 +705,6 @@ public String toString() { } } - private static final class WeightedConfigSource implements Weighted { - private final HelidonSourceWithPriority source; - private final ConfigContext context; - - private WeightedConfigSource(HelidonSourceWithPriority source, ConfigContext context) { - this.source = source; - this.context = context; - } - - @Override - public double weight() { - return source.weight(context); - } - - private ConfigSourceRuntimeImpl runtime(ConfigContextImpl context) { - return context.sourceRuntimeBase(source.unwrap()); - } - } - - private static final class HelidonSourceWithPriority { - private final ConfigSource configSource; - private final Double explicitWeight; - - private HelidonSourceWithPriority(ConfigSource configSource, Double explicitWeight) { - this.configSource = configSource; - this.explicitWeight = explicitWeight; - } - - ConfigSource unwrap() { - return configSource; - } - - double weight(ConfigContext context) { - // first - explicit priority. If configured by user, return it - if (null != explicitWeight) { - return explicitWeight; - } - - // ordinal from data - return context.sourceRuntime(configSource) - .node("config_priority") - .flatMap(node -> node.value() - .map(Double::parseDouble)) - .orElseGet(() -> { - // the config source does not have an ordinal configured, I need to get it from other places - return Weights.find(configSource, Weighted.DEFAULT_WEIGHT); - }); - } - } - private static class LoadedFilterProvider implements Function { private final ConfigFilter filter; diff --git a/config/config/src/main/java/io/helidon/config/Config.java b/config/config/src/main/java/io/helidon/config/Config.java index f34c4a88d38..efdc44966e6 100644 --- a/config/config/src/main/java/io/helidon/config/Config.java +++ b/config/config/src/main/java/io/helidon/config/Config.java @@ -1020,8 +1020,8 @@ static String escapeName(String name) { * @return unescaped name */ static String unescapeName(String escapedName) { - return escapedName.replaceAll("~1", ".") - .replaceAll("~0", "~"); + return escapedName.replace("~1", ".") + .replace("~0", "~"); } } @@ -1635,8 +1635,14 @@ default Builder sources(Supplier configSource, * @see #config(Config) */ default Builder metaConfig() { - MetaConfig.metaConfig() - .ifPresent(this::config); + try { + MetaConfig.metaConfig() + .ifPresent(this::config); + } catch (MetaConfigException e) { + System.getLogger(getClass().getName()) + .log(System.Logger.Level.WARNING, "Failed to load SE meta-configuration," + + " please make sure it has correct format.", e); + } return this; } diff --git a/config/config/src/main/java/io/helidon/config/ConfigDiff.java b/config/config/src/main/java/io/helidon/config/ConfigDiff.java index 13126dbf629..156c5652121 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigDiff.java +++ b/config/config/src/main/java/io/helidon/config/ConfigDiff.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020 Oracle and/or its affiliates. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,8 +86,8 @@ private static boolean notEqual(Config left, Config right) { } private static Optional value(Config node) { - if (node instanceof AbstractConfigImpl) { - return ((AbstractConfigImpl) node).value(); + if (node instanceof AbstractConfigImpl abstractConfig) { + return abstractConfig.value(); } return node.asString().asOptional(); } diff --git a/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java b/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java index f920e4f8883..30e36d84dd2 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,8 +109,8 @@ ConfigKeyImpl child(String key) { @Override public ConfigKeyImpl child(io.helidon.common.config.Config.Key key) { final List path; - if (key instanceof ConfigKeyImpl) { - path = ((ConfigKeyImpl) key).path; + if (key instanceof ConfigKeyImpl configKey) { + path = configKey.path; } else { path = new LinkedList<>(); while (!key.isRoot()) { diff --git a/config/config/src/main/java/io/helidon/config/ConfigProvider.java b/config/config/src/main/java/io/helidon/config/ConfigProvider.java index d6ed2e86620..c4b2912bd9d 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigProvider.java +++ b/config/config/src/main/java/io/helidon/config/ConfigProvider.java @@ -16,10 +16,13 @@ package io.helidon.config; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import io.helidon.common.config.Config; import io.helidon.common.config.ConfigException; @@ -48,13 +51,17 @@ class ConfigProvider implements Config { .config(metaConfig.get().metaConfiguration()) .update(it -> configSources.get() .forEach(it::addSource)) + .update(it -> defaultConfigSources(it, configParsers)) .disableParserServices() .update(it -> configParsers.get() .forEach(it::addParser)) .disableFilterServices() .update(it -> configFilters.get() .forEach(it::addFilter)) - .disableMapperServices() + //.disableMapperServices() + // cannot do this for now, removed ConfigMapperProvider from service loaded services, config does it on its + // own + // ObjectConfigMapper is before EnumMapper, and both are before essential and built-in .update(it -> configMappers.get() .forEach(it::addMapper)) .build(); @@ -72,12 +79,12 @@ public Config root() { } @Override - public Config get(String key) throws ConfigException { + public Config get(String key) throws io.helidon.common.config.ConfigException { return config.get(key); } @Override - public Config detach() throws ConfigException { + public Config detach() throws io.helidon.common.config.ConfigException { return config.detach(); } @@ -107,27 +114,30 @@ public boolean hasValue() { } @Override - public ConfigValue as(Class type) { + public io.helidon.common.config.ConfigValue as(Class type) { return config.as(type); } @Override - public ConfigValue map(Function mapper) { + public io.helidon.common.config.ConfigValue map(Function mapper) { return config.map(mapper); } @Override - public ConfigValue> asList(Class type) throws ConfigException { + public io.helidon.common.config.ConfigValue> asList(Class type) throws + io.helidon.common.config.ConfigException { return config.asList(type); } @Override - public ConfigValue> mapList(Function mapper) throws ConfigException { + public io.helidon.common.config.ConfigValue> mapList(Function mapper) throws + io.helidon.common.config.ConfigException { return config.mapList(mapper); } @Override - public ConfigValue> asNodeList() throws ConfigException { + public io.helidon.common.config.ConfigValue> asNodeList() throws + io.helidon.common.config.ConfigException { return config.asNodeList(); } @@ -135,4 +145,23 @@ public ConfigValue> asNodeList() throws ConfigExcepti public ConfigValue> asMap() throws ConfigException { return config.asMap(); } + + private void defaultConfigSources(io.helidon.config.Config.Builder configBuilder, + Supplier> configParsers) { + + Set supportedSuffixes = configParsers.get() + .stream() + .map(ConfigParser::supportedSuffixes) + .flatMap(List::stream) + .collect(Collectors.toSet()); + + // profile source(s) before defaults + MetaConfigFinder.profile() + .ifPresent(profile -> MetaConfigFinder.configSources(new ArrayList<>(supportedSuffixes), + profile)); + // default config source(s) + MetaConfigFinder.configSources(new ArrayList<>(supportedSuffixes)) + .forEach(configBuilder::addSource); + + } } diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java index 7c91359737f..f95c91bfbeb 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,16 +83,15 @@ class ConfigSourceRuntimeImpl implements ConfigSourceRuntime { // content source AtomicReference lastStamp = new AtomicReference<>(); - if (configSource instanceof ParsableSource) { + if (configSource instanceof ParsableSource parsableSource) { // eager parsable config source - reloader = new ParsableConfigSourceReloader(configContext, (ParsableSource) source, lastStamp); + reloader = new ParsableConfigSourceReloader(configContext, parsableSource, lastStamp); singleNodeFunction = objectNodeToSingleNode(); - } else if (configSource instanceof NodeConfigSource) { + } else if (configSource instanceof NodeConfigSource nodeConfigSource) { // eager node config source - reloader = new NodeConfigSourceReloader((NodeConfigSource) source, lastStamp); + reloader = new NodeConfigSourceReloader(nodeConfigSource, lastStamp); singleNodeFunction = objectNodeToSingleNode(); - } else if (configSource instanceof LazyConfigSource) { - LazyConfigSource lazySource = (LazyConfigSource) source; + } else if (configSource instanceof LazyConfigSource lazySource) { // lazy config source reloader = Optional::empty; singleNodeFunction = lazySource::node; @@ -143,8 +142,7 @@ class ConfigSourceRuntimeImpl implements ConfigSourceRuntime { } } - if (!changesSupported && (configSource instanceof EventConfigSource)) { - EventConfigSource event = (EventConfigSource) source; + if (!changesSupported && (configSource instanceof EventConfigSource event)) { changesSupported = true; changesRunnable = () -> event.onChange((key, config) -> listeners.forEach(it -> it.accept(key, config))); } @@ -222,8 +220,8 @@ private synchronized void initialLoad() { } // we may have media type mapping per node configured as well - if (configSource instanceof AbstractConfigSource) { - loadedData = loadedData.map(it -> ((AbstractConfigSource) configSource) + if (configSource instanceof AbstractConfigSource abstractConfigSource) { + loadedData = loadedData.map(it -> abstractConfigSource .processNodeMapping(configContext::findParser, ConfigKeyImpl.of(), it)); } diff --git a/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java b/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java index 746ccd5338a..f93bc5ea638 100644 --- a/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java +++ b/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,24 +40,30 @@ * * These conversions are intended to maximize ease-of-use for authors of config sources so the values need not be * upper-cased nor punctuated with underscores rather than the more conventional (in config at least) hyphen. - *

    *

    * The only hardship this imposes is if a confusingly-designed enum has values which differ only in case and the * string in the config source does not exactly match one of the enum value names. In such cases * the mapper will be unable to choose which enum value matches an ambiguous string. A developer faced with this * problem can simply provide her own explicit config mapping for that enum, for instance as a function parameter to * {@code Config#as}. - *

    - * */ @Weight(EnumMapperProvider.WEIGHT) -class EnumMapperProvider implements ConfigMapperProvider { +public class EnumMapperProvider implements ConfigMapperProvider { /** * Priority with which the enum mapper provider is added to the collection of providers (user- and Helidon-provided). */ static final double WEIGHT = Weighted.DEFAULT_WEIGHT; + /** + * Required constructor for {@link java.util.ServiceLoader}. + */ + public EnumMapperProvider() { + /* + This is now a "proper" service, to make this available also when using ServiceRegistry + */ + } + @Override public Map, Function> mappers() { return Map.of(); diff --git a/config/config/src/main/java/io/helidon/config/MetaConfig.java b/config/config/src/main/java/io/helidon/config/MetaConfig.java index afbe1b9c502..f78a7df2edb 100644 --- a/config/config/src/main/java/io/helidon/config/MetaConfig.java +++ b/config/config/src/main/java/io/helidon/config/MetaConfig.java @@ -22,7 +22,6 @@ import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; -import java.util.function.Function; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.media.type.MediaType; @@ -241,18 +240,6 @@ static List configSources(Config metaConfig) { return configSources; } - // only interested in config source - static List configSources(Function supportedMediaType, List supportedSuffixes) { - Optional metaConfigOpt = metaConfig(); - - return metaConfigOpt - .map(MetaConfig::configSources) - .orElseGet(() -> MetaConfigFinder.findConfigSource(supportedMediaType, supportedSuffixes) - .map(List::of) - .orElseGet(List::of)); - - } - private static Config createDefault() { // use defaults Config.Builder builder = Config.builder(); diff --git a/config/config/src/main/java/io/helidon/config/MetaConfigException.java b/config/config/src/main/java/io/helidon/config/MetaConfigException.java new file mode 100644 index 00000000000..e0be98878fe --- /dev/null +++ b/config/config/src/main/java/io/helidon/config/MetaConfigException.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config; + +/** + * Exception is thrown if problems are found while processing meta config. + */ +class MetaConfigException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Constructor with the detailed message. + * + * @param message the message + */ + MetaConfigException(String message) { + super(message); + } + + /** + * Constructor with the detailed message. + * + * @param message the message + * @param cause the cause + */ + MetaConfigException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java b/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java index ee95b176da7..f2a75d9805c 100644 --- a/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java +++ b/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,8 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; @@ -94,13 +96,7 @@ static Optional findConfigSource(Function supp return findSource(supportedMediaType, cl, CONFIG_PREFIX, "config source", supportedSuffixes); } - private static Optional findMetaConfigSource(Function supportedMediaType, - List supportedSuffixes) { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - Optional source; - - // check if meta configuration is configured using system property - String metaConfigFile = System.getProperty(META_CONFIG_SYSTEM_PROPERTY); + static Optional profile() { // check name of the profile String profileName = System.getenv(CONFIG_PROFILE_ENVIRONMENT_VARIABLE); if (profileName == null) { @@ -109,6 +105,44 @@ private static Optional findMetaConfigSource(Function configSources(List supportedSuffixes, + String profileName) { + return supportedSuffixes.stream() + .flatMap(suffix -> configSources("application-" + profileName + "." + suffix)) + .collect(Collectors.toUnmodifiableList()); + } + + static List configSources(List supportedSuffixes) { + return supportedSuffixes.stream() + .flatMap(suffix -> configSources("application." + suffix)) + .collect(Collectors.toUnmodifiableList()); + } + + static Stream configSources(String fileName) { + // we look on file system and on classpath, file system is more important + return Stream.concat(findFile(fileName, "default config source") + .stream(), + findAllClasspath(fileName)); + } + + private static Stream findAllClasspath(String fileName) { + return ConfigSources.classpathAll(fileName) + .stream() + .map(UrlConfigSource.Builder::build); + } + + private static Optional findMetaConfigSource(Function supportedMediaType, + List supportedSuffixes) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Optional source; + + // check if meta configuration is configured using system property + String metaConfigFile = System.getProperty(META_CONFIG_SYSTEM_PROPERTY); + // check name of the profile + String profileName = profile().orElse(null); if (metaConfigFile != null && profileName != null) { // we have both profile name and meta configuration file defined diff --git a/config/config/src/main/java/io/helidon/config/MetaProviders.java b/config/config/src/main/java/io/helidon/config/MetaProviders.java index 1c30190f5a2..9b1abd3fb69 100644 --- a/config/config/src/main/java/io/helidon/config/MetaProviders.java +++ b/config/config/src/main/java/io/helidon/config/MetaProviders.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -113,7 +113,7 @@ static ConfigSource configSource(String type, Config config) { .filter(provider -> provider.supports(type)) .findFirst() .map(provider -> provider.create(type, config)) - .orElseThrow(() -> new IllegalArgumentException("Config source of type " + type + " is not supported." + .orElseThrow(() -> new MetaConfigException("Config source of type " + type + " is not supported." + " Supported types: " + SUPPORTED_CONFIG_SOURCES)); } @@ -122,7 +122,7 @@ static List configSources(String type, Config sourceProperties) { .filter(provider -> provider.supports(type)) .findFirst() .map(provider -> provider.createMulti(type, sourceProperties)) - .orElseThrow(() -> new IllegalArgumentException("Config source of type " + type + " is not supported." + .orElseThrow(() -> new MetaConfigException("Config source of type " + type + " is not supported." + " Supported types: " + SUPPORTED_CONFIG_SOURCES)); } @@ -131,7 +131,7 @@ static OverrideSource overrideSource(String type, Config config) { .filter(provider -> provider.supports(type)) .findFirst() .map(provider -> provider.create(type, config)) - .orElseThrow(() -> new IllegalArgumentException("Config source of type " + type + " is not supported." + .orElseThrow(() -> new MetaConfigException("Config source of type " + type + " is not supported." + " Supported types: " + SUPPORTED_OVERRIDE_SOURCES)); } @@ -140,7 +140,7 @@ static PollingStrategy pollingStrategy(String type, Config config) { .filter(provider -> provider.supports(type)) .findFirst() .map(provider -> provider.create(type, config)) - .orElseThrow(() -> new IllegalArgumentException("Polling strategy of type " + type + " is not supported." + .orElseThrow(() -> new MetaConfigException("Polling strategy of type " + type + " is not supported." + " Supported types: " + SUPPORTED_POLLING_STRATEGIES)); } @@ -149,7 +149,7 @@ static RetryPolicy retryPolicy(String type, Config config) { .filter(provider -> provider.supports(type)) .findFirst() .map(provider -> provider.create(type, config)) - .orElseThrow(() -> new IllegalArgumentException("Retry policy of type " + type + " is not supported." + .orElseThrow(() -> new MetaConfigException("Retry policy of type " + type + " is not supported." + " Supported types: " + SUPPORTED_RETRY_POLICIES)); } @@ -158,7 +158,7 @@ public static ChangeWatcher changeWatcher(String type, Config config) { .filter(provider -> provider.supports(type)) .findFirst() .map(provider -> provider.create(type, config)) - .orElseThrow(() -> new IllegalArgumentException("Change watcher of type " + type + " is not supported." + .orElseThrow(() -> new MetaConfigException("Change watcher of type " + type + " is not supported." + " Supported types: " + SUPPORTED_CHANGE_WATCHERS)); } diff --git a/config/config/src/main/java/io/helidon/config/UrlConfigSource.java b/config/config/src/main/java/io/helidon/config/UrlConfigSource.java index 24ebeda0c94..c936442e014 100644 --- a/config/config/src/main/java/io/helidon/config/UrlConfigSource.java +++ b/config/config/src/main/java/io/helidon/config/UrlConfigSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -139,8 +139,8 @@ public Optional load() throws ConfigException { try { URLConnection urlConnection = url.openConnection(); - if (urlConnection instanceof HttpURLConnection) { - return httpContent((HttpURLConnection) urlConnection); + if (urlConnection instanceof HttpURLConnection httpURLConnection) { + return httpContent(httpURLConnection); } else { return genericContent(urlConnection); } diff --git a/config/config/src/main/java/io/helidon/config/UrlHelper.java b/config/config/src/main/java/io/helidon/config/UrlHelper.java index 1267390e555..673c4564a32 100644 --- a/config/config/src/main/java/io/helidon/config/UrlHelper.java +++ b/config/config/src/main/java/io/helidon/config/UrlHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,8 +44,7 @@ static Optional dataStamp(URL url) { // the URL may not be an HTTP URL try { URLConnection urlConnection = url.openConnection(); - if (urlConnection instanceof HttpURLConnection) { - HttpURLConnection connection = (HttpURLConnection) urlConnection; + if (urlConnection instanceof HttpURLConnection connection) { try { connection.setRequestMethod(HEAD_METHOD); if (STATUS_NOT_FOUND == connection.getResponseCode()) { diff --git a/config/config/src/main/java/module-info.java b/config/config/src/main/java/module-info.java index 03790f9abcf..87e4ad08678 100644 --- a/config/config/src/main/java/module-info.java +++ b/config/config/src/main/java/module-info.java @@ -51,6 +51,8 @@ with io.helidon.config.PropertiesConfigParser; provides io.helidon.common.config.spi.ConfigProvider with io.helidon.config.HelidonConfigProvider; + provides io.helidon.config.spi.ConfigMapperProvider + with io.helidon.config.EnumMapperProvider; // needed when running with modules - to make private methods accessible opens io.helidon.config to weld.core.impl, io.helidon.microprofile.cdi; diff --git a/config/config/src/main/resources/META-INF/helidon/service.loader b/config/config/src/main/resources/META-INF/helidon/service.loader index d07172ed9b3..c951c168384 100644 --- a/config/config/src/main/resources/META-INF/helidon/service.loader +++ b/config/config/src/main/resources/META-INF/helidon/service.loader @@ -1,4 +1,6 @@ # List of service contracts we want to support either from service registry, or from service loader io.helidon.config.spi.ConfigParser io.helidon.config.spi.ConfigFilter -io.helidon.config.spi.ConfigMapperProvider +# This cannot be done for now, as ObjectConfigMapper ends up before built-ins when +# we disable mapper services +# io.helidon.config.spi.ConfigMapperProvider diff --git a/config/encryption/pom.xml b/config/encryption/pom.xml index 6e6fb37e978..5e138da452a 100644 --- a/config/encryption/pom.xml +++ b/config/encryption/pom.xml @@ -23,7 +23,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-config-encryption diff --git a/config/etcd/pom.xml b/config/etcd/pom.xml index bf74074c3b9..64ef0b66e28 100644 --- a/config/etcd/pom.xml +++ b/config/etcd/pom.xml @@ -23,7 +23,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-etcd Helidon Config Etcd diff --git a/config/git/pom.xml b/config/git/pom.xml index 6d853422666..4e16be92b40 100644 --- a/config/git/pom.xml +++ b/config/git/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-git Helidon Config Git diff --git a/config/hocon-mp/pom.xml b/config/hocon-mp/pom.xml index 4afccf75e47..ed3e599d861 100644 --- a/config/hocon-mp/pom.xml +++ b/config/hocon-mp/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-hocon-mp Helidon Config HOCON MP diff --git a/config/hocon-mp/src/main/java/io/helidon/config/hocon/mp/HoconMpConfigIncluder.java b/config/hocon-mp/src/main/java/io/helidon/config/hocon/mp/HoconMpConfigIncluder.java index efde0b25d11..4cf882f7452 100644 --- a/config/hocon-mp/src/main/java/io/helidon/config/hocon/mp/HoconMpConfigIncluder.java +++ b/config/hocon-mp/src/main/java/io/helidon/config/hocon/mp/HoconMpConfigIncluder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,6 @@ public ConfigIncluder withFallback(ConfigIncluder fallback) { @Override public ConfigObject include(ConfigIncludeContext context, String what) { - new Exception().printStackTrace(); LOGGER.log(TRACE, String.format("Received request to include resource %s, %s", what, context.parseOptions().getOriginDescription())); @@ -71,7 +70,6 @@ private ConfigObject parseHoconFromUrl(String includeName) { URL includeUrl; try { includeUrl = new URL(includePath); - System.out.println("includeURL: " + includeUrl); } catch (MalformedURLException e) { LOGGER.log(WARNING, String.format("Unable to create include Url for: %s with error: %s", includePath, e.getMessage())); @@ -88,7 +86,6 @@ private ConfigObject parseHoconFromUrl(String includeName) { private ConfigObject parseHoconFromPath(String includeName) { Path path = relativePath.resolve(includeName); if (Files.exists(path) && Files.isReadable(path) && !Files.isDirectory(path)) { - System.out.println("Path: " + path); try (BufferedReader reader = Files.newBufferedReader(path, charset)) { Config typesafeConfig = ConfigFactory.parseReader(reader, parseOptions); return typesafeConfig.root(); diff --git a/config/hocon/pom.xml b/config/hocon/pom.xml index 203ac5e7135..6349198366b 100644 --- a/config/hocon/pom.xml +++ b/config/hocon/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-hocon Helidon Config HOCON diff --git a/config/hocon/src/main/java/io/helidon/config/hocon/HoconConfigParser.java b/config/hocon/src/main/java/io/helidon/config/hocon/HoconConfigParser.java index b09abb65678..536fc19022b 100644 --- a/config/hocon/src/main/java/io/helidon/config/hocon/HoconConfigParser.java +++ b/config/hocon/src/main/java/io/helidon/config/hocon/HoconConfigParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -155,10 +155,10 @@ private static ObjectNode fromConfig(ConfigObject config) { ObjectNode.Builder builder = ObjectNode.builder(); config.forEach((unescapedKey, value) -> { String key = io.helidon.config.Config.Key.escapeName(unescapedKey); - if (value instanceof ConfigList) { - builder.addList(key, fromList((ConfigList) value)); - } else if (value instanceof ConfigObject) { - builder.addObject(key, fromConfig((ConfigObject) value)); + if (value instanceof ConfigList configList) { + builder.addList(key, fromList(configList)); + } else if (value instanceof ConfigObject configObject) { + builder.addObject(key, fromConfig(configObject)); } else { try { Object unwrapped = value.unwrapped(); @@ -181,10 +181,10 @@ private static ObjectNode fromConfig(ConfigObject config) { private static ListNode fromList(ConfigList list) { ListNode.Builder builder = ListNode.builder(); list.forEach(value -> { - if (value instanceof ConfigList) { - builder.addList(fromList((ConfigList) value)); - } else if (value instanceof ConfigObject) { - builder.addObject(fromConfig((ConfigObject) value)); + if (value instanceof ConfigList configList) { + builder.addList(fromList(configList)); + } else if (value instanceof ConfigObject configObject) { + builder.addObject(fromConfig(configObject)); } else { try { Object unwrapped = value.unwrapped(); diff --git a/config/metadata-processor/pom.xml b/config/metadata-processor/pom.xml deleted file mode 100644 index 66427109e6a..00000000000 --- a/config/metadata-processor/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - 4.0.0 - - io.helidon.config - helidon-config-project - 4.1.0-SNAPSHOT - - helidon-config-metadata-processor - Helidon Config Metadata Annotation Processor - - - Configuration Metadata processing. - - - - - io.helidon.common - helidon-common-types - - - io.helidon.common.processor - helidon-common-processor - - - org.junit.jupiter - junit-jupiter-api - test - - - org.hamcrest - hamcrest-all - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - -proc:none - - - - - diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredAnnotation.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredAnnotation.java deleted file mode 100644 index 8c41f8dc0a3..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredAnnotation.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; - -import io.helidon.common.types.Annotation; -import io.helidon.common.types.TypeInfo; - -import static io.helidon.config.metadata.processor.UsedTypes.CONFIGURED; -import static io.helidon.config.metadata.processor.UsedTypes.DESCRIPTION; -import static io.helidon.config.metadata.processor.UsedTypes.PROTOTYPE_PROVIDES; - -record ConfiguredAnnotation(Optional description, - Optional prefix, - List provides, - boolean root, - boolean ignoreBuildMethod) { - - static ConfiguredAnnotation createMeta(Annotation annotation) { - return new ConfiguredAnnotation( - annotation.stringValue("description").filter(Predicate.not(String::isBlank)), - annotation.stringValue("prefix").filter(Predicate.not(String::isBlank)), - toProvidesMeta(annotation), - annotation.booleanValue("root").orElse(false), - annotation.booleanValue("ignoreBuildMethod").orElse(false) - ); - } - - static ConfiguredAnnotation createBuilder(TypeInfo blueprint) { - Optional config = blueprint.findAnnotation(CONFIGURED) - .flatMap(Annotation::stringValue) - .filter(Predicate.not(String::isBlank)); - boolean isRoot = config.isPresent() && blueprint.findAnnotation(CONFIGURED) - .flatMap(it -> it.booleanValue("root")) - .orElse(true); - - return new ConfiguredAnnotation( - blueprint.findAnnotation(DESCRIPTION).flatMap(Annotation::stringValue), - config, - toProvidesBuilder(blueprint), - isRoot, - false - ); - } - - private static List toProvidesBuilder(TypeInfo blueprint) { - return blueprint.findAnnotation(PROTOTYPE_PROVIDES) - .flatMap(Annotation::stringValues) - .stream() - .flatMap(List::stream) - .toList(); - } - - private static List toProvidesMeta(Annotation annotation) { - return annotation.stringValues("provides") - .orElseGet(List::of); - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredOptionData.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredOptionData.java deleted file mode 100644 index e1320526d23..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredOptionData.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.TypeElement; -import javax.lang.model.util.Elements; - -import io.helidon.common.types.Annotation; -import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypeNames; -import io.helidon.common.types.TypedElementInfo; - -import static io.helidon.config.metadata.processor.TypeHandlerBase.UNCONFIGURED_OPTION; -import static io.helidon.config.metadata.processor.TypeHandlerBase.javadoc; -import static io.helidon.config.metadata.processor.UsedTypes.DEPRECATED; -import static io.helidon.config.metadata.processor.UsedTypes.DESCRIPTION; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_ALLOWED_VALUE; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_ALLOWED_VALUES; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_CONFIGURED; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_DEFAULT; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_DEFAULT_BOOLEAN; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_DEFAULT_CODE; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_DEFAULT_DOUBLE; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_DEFAULT_INT; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_DEFAULT_LONG; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_DEFAULT_METHOD; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_PROVIDER; -import static io.helidon.config.metadata.processor.UsedTypes.OPTION_REQUIRED; -import static java.util.function.Predicate.not; - -final class ConfiguredOptionData { - private final List allowedValues = new LinkedList<>(); - - private boolean configured = true; - private String name; - private TypeName type; - private String description; - private boolean required; - private String defaultValue; - private boolean experimental; - private boolean provider; - private boolean deprecated; - private boolean merge; - private String kind = "VALUE"; - private TypeName providerType; - - // create from @ConfiguredOption in config-metadata - static ConfiguredOptionData createMeta(ProcessingEnvironment aptEnv, Annotation option) { - ConfiguredOptionData result = new ConfiguredOptionData(); - - option.booleanValue("configured").ifPresent(result::configured); - option.stringValue("key").filter(not(String::isBlank)).ifPresent(result::name); - option.stringValue("description").filter(not(String::isBlank)).ifPresent(result::description); - option.stringValue().filter(not(UNCONFIGURED_OPTION::equals)).ifPresent(result::defaultValue); - option.booleanValue("experimental").ifPresent(result::experimental); - option.booleanValue("required").ifPresent(result::required); - option.booleanValue("mergeWithParent").ifPresent(result::merge); - option.typeValue("type").ifPresent(result::type); - option.stringValue("kind").ifPresent(result::kind); - option.booleanValue("provider").ifPresent(result::provider); - option.booleanValue("deprecated").ifPresent(result::deprecated); - option.annotationValues("allowedValues") - .or(() -> option.annotationValue("allowedValue").map(List::of)) - .stream() - .flatMap(List::stream) - .map(AllowedValue::create) - .forEach(result::addAllowedValue); - - if (result.allowedValues.isEmpty()) { - // if enum, fill this in - Elements aptElements = aptEnv.getElementUtils(); - TypeElement typeElement = aptElements - .getTypeElement(option.typeValue("type").orElse(TypeNames.STRING).fqName()); - if (typeElement != null && typeElement.getKind() == ElementKind.ENUM) { - result.allowedValues.addAll(allowedValues(aptElements, typeElement)); - } - } - - return result; - } - - // create from Option annotations in builder-api - static ConfiguredOptionData createBuilder(TypedElementInfo element) { - ConfiguredOptionData result = new ConfiguredOptionData(); - - Optional optionConfigured = element.findAnnotation(OPTION_CONFIGURED); - optionConfigured.flatMap(Annotation::stringValue).filter(not(String::isBlank)) - .ifPresent(result::name); - optionConfigured.flatMap(it -> it.booleanValue("merge")) - .ifPresent(result::merge); - - element.findAnnotation(DESCRIPTION).flatMap(Annotation::stringValue).ifPresent(result::description); - element.findAnnotation(OPTION_REQUIRED).ifPresent(it -> result.required(true)); - element.findAnnotation(OPTION_PROVIDER).ifPresent(it -> { - it.typeValue().ifPresent(result::providerType); - result.provider(true); - }); - element.findAnnotation(DEPRECATED).ifPresent(it -> result.deprecated(true)); - element.findAnnotation(OPTION_ALLOWED_VALUES) - .flatMap(Annotation::annotationValues) - .or(() -> element.findAnnotation(OPTION_ALLOWED_VALUE).map(List::of)) - .stream() - .flatMap(List::stream) - .map(AllowedValue::create) - .forEach(result::addAllowedValue); - - Optional defaultValues = element.findAnnotation(OPTION_DEFAULT) - .or(() -> element.findAnnotation(OPTION_DEFAULT_INT)) - .or(() -> element.findAnnotation(OPTION_DEFAULT_BOOLEAN)) - .or(() -> element.findAnnotation(OPTION_DEFAULT_LONG)) - .or(() -> element.findAnnotation(OPTION_DEFAULT_DOUBLE)); - if (defaultValues.isPresent()) { - List strings = defaultValues.get().stringValues().orElseGet(List::of); - result.defaultValue(String.join(", ", strings)); - } else if (element.hasAnnotation(OPTION_DEFAULT_METHOD)) { - Annotation annotation = element.annotation(OPTION_DEFAULT_METHOD); - TypeName type = annotation.typeValue("type") - .filter(Predicate.not(OPTION_DEFAULT_METHOD::equals)) - .or(element::enclosingType) - .orElse(null); - String value = annotation.stringValue().orElse(null); - if (value != null) { - // this should always be true, as it is mandatory - if (type == null) { - result.defaultValue(value + "()"); - } else { - result.defaultValue(type.fqName() + "." + value + "()"); - } - } - } else if (element.hasAnnotation(OPTION_DEFAULT_CODE)) { - element.annotation(OPTION_DEFAULT_CODE).stringValue().ifPresent(result::defaultValue); - } - - return result; - } - - List allowedValues() { - return allowedValues; - } - - String name() { - return name; - } - - TypeName type() { - return type; - } - - String description() { - return description; - } - - boolean optional() { - return !required; - } - - String defaultValue() { - return defaultValue; - } - - boolean experimental() { - return experimental; - } - - boolean provider() { - return provider; - } - - TypeName providerType() { - return providerType; - } - - boolean deprecated() { - return deprecated; - } - - boolean merge() { - return merge; - } - - String kind() { - return kind; - } - - boolean configured() { - return configured; - } - - void type(TypeName type) { - this.type = type; - } - - void name(String name) { - this.name = name; - } - - void description(String description) { - this.description = description; - } - - void required(boolean required) { - this.required = required; - } - - void defaultValue(String defaultValue) { - this.defaultValue = defaultValue; - } - - void experimental(boolean experimental) { - this.experimental = experimental; - } - - void provider(boolean provider) { - this.provider = provider; - } - - void providerType(TypeName provider) { - this.providerType = provider; - } - - void deprecated(boolean deprecated) { - this.deprecated = deprecated; - } - - void merge(boolean merge) { - this.merge = merge; - } - - void kind(String kind) { - this.kind = kind; - } - - void addAllowedValue(AllowedValue value) { - this.allowedValues.add(value); - } - - void configured(boolean configured) { - this.configured = configured; - } - - private static List allowedValues(Elements aptElements, TypeElement typeElement) { - if (typeElement != null && typeElement.getKind() == ElementKind.ENUM) { - return typeElement.getEnclosedElements() - .stream() - .filter(element -> element.getKind().equals(ElementKind.ENUM_CONSTANT)) - .map(element -> new AllowedValue(element.toString(), javadoc(aptElements.getDocComment(element)))) - .collect(Collectors.toList()); - } - return List.of(); - } - - static final class AllowedValue { - private String value; - private String description; - - private AllowedValue() { - } - - AllowedValue(String value, String description) { - this.value = value; - this.description = description; - } - - String value() { - return value; - } - - String description() { - return description; - } - - private static AllowedValue create(Annotation annotation) { - AllowedValue result = new AllowedValue(); - - annotation.stringValue().ifPresent(it -> result.value = it); - annotation.stringValue("description").filter(not(String::isBlank)).ifPresent(it -> result.description = it); - - return result; - } - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredType.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredType.java deleted file mode 100644 index 6b20030615b..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredType.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import io.helidon.common.types.TypeName; - -final class ConfiguredType { - private final Set allProperties = new HashSet<>(); - private final List producerMethods = new LinkedList<>(); - /* - * The type that is built by a builder, or created using create method. - */ - private final TypeName targetClass; - /* - The type we are processing that has @Configured annotation - */ - private final TypeName annotatedClass; - private final List inherited = new LinkedList<>(); - private final ConfiguredAnnotation configured; - - ConfiguredType(ConfiguredAnnotation configured, TypeName annotatedClass, TypeName targetClass, boolean typeDefinition) { - this.annotatedClass = annotatedClass; - this.targetClass = targetClass; - this.configured = configured; - } - - ConfiguredType addProducer(ProducerMethod producer) { - producerMethods.add(producer); - return this; - } - - ConfiguredType addProperty(ConfiguredProperty property) { - allProperties.add(property); - return this; - } - - List producers() { - return producerMethods; - } - - Set properties() { - return allProperties; - } - - String targetClass() { - return targetClass.fqName(); - } - - String annotatedClass() { - return annotatedClass.fqName(); - } - - boolean standalone() { - return configured.root(); - } - - String prefix() { - return configured.prefix().orElse(null); - } - - void write(JArray typeArray) { - JObject typeObject = new JObject(); - - typeObject.add("type", targetClass()); - typeObject.add("annotatedType", annotatedClass()); - if (standalone()) { - typeObject.add("standalone", true); - } - configured.prefix().ifPresent(it -> typeObject.add("prefix", it)); - configured.description().ifPresent(it -> typeObject.add("description", it)); - - if (!inherited.isEmpty()) { - typeObject.add("inherits", inherited.stream() - .map(TypeName::fqName) - .toList()); - } - - if (!configured.provides().isEmpty()) { - typeObject.add("provides", configured.provides()); - } - - if (!producerMethods.isEmpty()) { - typeObject.add("producers", producerMethods.stream() - .map(Object::toString) - .collect(Collectors.toList())); - } - - JArray options = new JArray(); - for (ConfiguredProperty property : allProperties) { - writeProperty(options, "", property); - } - typeObject.add("options", options); - - typeArray.add(typeObject); - } - - @Override - public String toString() { - return targetClass.fqName(); - } - - void addInherited(TypeName classOrIface) { - inherited.add(classOrIface); - } - - private static String paramsToString(List params) { - return params.stream() - .map(TypeName::resolvedName) - .collect(Collectors.joining(", ")); - } - - private void writeProperty(JArray optionsBuilder, - String prefix, - ConfiguredProperty property) { - - JObject optionBuilder = new JObject(); - if (property.key() != null && !property.key.isBlank()) { - optionBuilder.add("key", prefix(prefix, property.key())); - } - if (!"java.lang.String".equals(property.type)) { - optionBuilder.add("type", property.type()); - } - optionBuilder.add("description", property.description()); - if (property.defaultValue() != null) { - optionBuilder.add("defaultValue", property.defaultValue()); - } - if (property.experimental) { - optionBuilder.add("experimental", true); - } - if (!property.optional) { - optionBuilder.add("required", true); - } - if (!property.kind().equals("VALUE")) { - optionBuilder.add("kind", property.kind()); - } - if (property.provider) { - optionBuilder.add("provider", true); - optionBuilder.add("providerType", property.providerType.fqName()); - } - if (property.deprecated()) { - optionBuilder.add("deprecated", true); - } - if (property.merge()) { - optionBuilder.add("merge", true); - } - String method = property.builderMethod(); - if (method != null) { - optionBuilder.add("method", method); - } - if (property.configuredType != null) { - String finalPrefix; - if (property.kind().equals("LIST")) { - finalPrefix = prefix(prefix(prefix, property.key()), "*"); - } else { - finalPrefix = prefix(prefix, property.key()); - } - property.configuredType.properties() - .forEach(it -> writeProperty(optionsBuilder, finalPrefix, it)); - } - if (!property.allowedValues.isEmpty()) { - JArray allowedValues = new JArray(); - - for (ConfiguredOptionData.AllowedValue allowedValue : property.allowedValues) { - allowedValues.add(new JObject() - .add("value", allowedValue.value()) - .add("description", allowedValue.description())); - } - - optionBuilder.add("allowedValues", allowedValues); - } - - optionsBuilder.add(optionBuilder); - } - - private String prefix(String currentPrefix, String newSuffix) { - if (currentPrefix.isEmpty()) { - return newSuffix; - } - return currentPrefix + "." + newSuffix; - } - - static final class ProducerMethod { - private final boolean isStatic; - private final TypeName owningClass; - private final String methodName; - private final List methodParams; - - ProducerMethod(boolean isStatic, TypeName owningClass, String methodName, List methodParams) { - this.isStatic = isStatic; - this.owningClass = owningClass; - this.methodName = methodName; - this.methodParams = methodParams; - } - - @Override - public String toString() { - return owningClass.fqName() - + "#" - + methodName + "(" - + paramsToString(methodParams) + ")"; - } - } - - static final class ConfiguredProperty { - private final String builderMethod; - private final String key; - private final String description; - private final String defaultValue; - private final String type; - private final boolean experimental; - private final boolean optional; - private final String kind; - private final boolean provider; - private final TypeName providerType; - private final boolean deprecated; - private final boolean merge; - private final List allowedValues; - // if this is a nested type - private ConfiguredType configuredType; - - ConfiguredProperty(String builderMethod, - String key, - String description, - String defaultValue, - TypeName type, - boolean experimental, - boolean optional, - String kind, - boolean provider, - TypeName providerType, - boolean deprecated, - boolean merge, - List allowedValues) { - this.builderMethod = builderMethod; - this.key = key; - this.description = description; - this.defaultValue = defaultValue; - this.type = type.fqName(); - this.experimental = experimental; - this.optional = optional; - this.kind = kind; - this.provider = provider; - this.providerType = providerType == null ? type : providerType; - this.deprecated = deprecated; - this.merge = merge; - this.allowedValues = allowedValues; - } - - String builderMethod() { - return builderMethod; - } - - String key() { - return key; - } - - String description() { - return description; - } - - String defaultValue() { - return defaultValue; - } - - String type() { - return type; - } - - boolean experimental() { - return experimental; - } - - boolean optional() { - return optional; - } - - String kind() { - return kind; - } - - boolean deprecated() { - return deprecated; - } - - boolean merge() { - return merge; - } - - void nestedType(ConfiguredType nested) { - this.configuredType = nested; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ConfiguredProperty that = (ConfiguredProperty) o; - return key.equals(that.key); - } - - @Override - public int hashCode() { - return Objects.hash(key); - } - - @Override - public String toString() { - return key; - } - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/JArray.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/JArray.java deleted file mode 100644 index a71c5193ad8..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/JArray.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.io.PrintWriter; -import java.util.LinkedList; -import java.util.List; - -class JArray { - private final List values = new LinkedList<>(); - - public void add(JObject object) { - values.add(object); - } - - public void write(PrintWriter metaWriter) { - metaWriter.write('['); - - for (int i = 0; i < values.size(); i++) { - values.get(i).write(metaWriter); - if (i < (values.size() - 1)) { - metaWriter.write(','); - } - } - - metaWriter.write(']'); - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/JObject.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/JObject.java deleted file mode 100644 index 8644a2c8c0c..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/JObject.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.io.PrintWriter; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - -class JObject { - private final Map stringValues = new TreeMap<>(); - private final Map booleanValues = new TreeMap<>(); - private final Map> stringListValues = new TreeMap<>(); - private final Map arrayValues = new TreeMap<>(); - - JObject add(String key, String value) { - stringValues.put(key, value); - return this; - } - - JObject add(String key, boolean value) { - booleanValues.put(key, value); - return this; - } - - JObject add(String key, JArray array) { - arrayValues.put(key, array); - return this; - } - - JObject add(String key, List values) { - stringListValues.put(key, values); - return this; - } - - void write(PrintWriter metaWriter) { - metaWriter.write('{'); - AtomicBoolean first = new AtomicBoolean(true); - - stringValues.forEach((key, value) -> { - writeNext(metaWriter, first); - metaWriter.write('\"'); - metaWriter.write(key); - metaWriter.write("\":\""); - metaWriter.write(escape(value)); - metaWriter.write('\"'); - }); - booleanValues.forEach((key, value) -> { - writeNext(metaWriter, first); - metaWriter.write('\"'); - metaWriter.write(key); - metaWriter.write("\":"); - metaWriter.write(String.valueOf(value)); - }); - stringListValues.forEach((key, value) -> { - writeNext(metaWriter, first); - metaWriter.write('\"'); - metaWriter.write(key); - metaWriter.write("\":["); - writeStringList(metaWriter, value); - metaWriter.write(']'); - }); - arrayValues.forEach((key, value) -> { - writeNext(metaWriter, first); - metaWriter.write('\"'); - metaWriter.write(key); - metaWriter.write("\":"); - value.write(metaWriter); - }); - - metaWriter.write('}'); - } - - private void writeStringList(PrintWriter metaWriter, List value) { - metaWriter.write(value.stream() - .map(this::escape) - .map(this::quote) - .collect(Collectors.joining(","))); - } - - private String quote(String value) { - return '"' + value + '"'; - } - - private String escape(String string) { - return string.replaceAll("\n", "\\\\n") - .replaceAll("\"", "\\\\\""); - } - - private void writeNext(PrintWriter metaWriter, AtomicBoolean first) { - if (first.get()) { - first.set(false); - return; - } - metaWriter.write(','); - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/Javadoc.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/Javadoc.java deleted file mode 100644 index e8870da444a..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/Javadoc.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.util.regex.Pattern; - -import static io.helidon.common.processor.GeneratorTools.capitalize; - -/* -Possible improvements: -- @link - create a proper javadoc reference (i.e. always fully qualified reference), such as: - {@link:io.helidon.common.Type#method(java.lang.String)}, so we can generate a nice reference for docs -- @value - if possible, find the actual value (string, int etc.) and add it as `thevalue` -- @see - create a proper javadoc reference (as for @link) - */ -final class Javadoc { - private static final Pattern JAVADOC_CODE = Pattern.compile("\\{@code (.*?)}"); - private static final Pattern JAVADOC_LINK = Pattern.compile("\\{@link (.*?)}"); - private static final Pattern JAVADOC_VALUE = Pattern.compile("\\{@value (.*?)}"); - private static final Pattern JAVADOC_SEE = Pattern.compile("\\{@see (.*?)}"); - - private Javadoc() { - } - - /** - * Parses a Javadoc comment (provided as a string) into text that can be used for display/docs of the configuration option. - *

    - * The following steps are done: - *

      - *
    • {@code @param} is stripped from the text
    • - *
    • Any {@code @code} section: the code tag is removed, and surrounded with {@code '}
    • - *
    • Any {@code @link} section: the link tag is removed
    • - *
    • Any {@code @value} section: the value tag is removed, {code #} is replaced with {@code .}
    • - *
    • Any {@code @see} section: the see tag is removed, prefixed with {@code See}, - * {code #} is replaced with {@code .}
    • - *
    • {@code @return} is stripped from the text, and the first letter is capitalized
    • - *
    - * - * @param docComment "raw" javadoc from the source code - * @return description of the option - */ - static String parse(String docComment) { - if (docComment == null) { - return ""; - } - - String javadoc = docComment; - int index = javadoc.indexOf("@param"); - if (index > -1) { - javadoc = docComment.substring(0, index); - } - // replace all {@code xxx} with 'xxx' - javadoc = JAVADOC_CODE.matcher(javadoc).replaceAll(it -> javadocCode(it.group(1))); - // replace all {@link ...} with just the name - javadoc = JAVADOC_LINK.matcher(javadoc).replaceAll(it -> javadocLink(it.group(1))); - // replace all {@value ...} with just the reference - javadoc = JAVADOC_VALUE.matcher(javadoc).replaceAll(it -> javadocValue(it.group(1))); - // replace all {@see ...} with just the reference - javadoc = JAVADOC_SEE.matcher(javadoc).replaceAll(it -> javadocSee(it.group(1))); - - index = javadoc.indexOf("@return"); - if (index > -1) { - javadoc = javadoc.substring(0, index) + capitalize(javadoc.substring(index + 8)); - } - - return javadoc.trim(); - } - - private static String javadocSee(String originalValue) { - return "See " + javadocValue(originalValue); - } - - private static String javadocCode(String originalValue) { - return '`' + originalValue + '`'; - } - - private static String javadocLink(String originalValue) { - return javadocValue(originalValue); - } - - private static String javadocValue(String originalValue) { - if (originalValue.startsWith("#")) { - return originalValue.substring(1); - } - return originalValue.replace('#', '.'); - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandler.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandler.java deleted file mode 100644 index 87cc0f8fbbf..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -interface TypeHandler { - /** - * Discover all options of the configured type. - */ - TypeHandlerResult handle(); -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerBase.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerBase.java deleted file mode 100644 index c9ba18cd82f..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerBase.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (c) 2023, 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; - -import javax.annotation.processing.Messager; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.TypeElement; -import javax.lang.model.util.Elements; - -import io.helidon.common.processor.TypeInfoFactory; -import io.helidon.common.types.TypeInfo; -import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementInfo; - -import static io.helidon.config.metadata.processor.UsedTypes.COMMON_CONFIG; -import static io.helidon.config.metadata.processor.UsedTypes.CONFIG; - -abstract class TypeHandlerBase { - static final String UNCONFIGURED_OPTION = "io.helidon.config.metadata.ConfiguredOption.UNCONFIGURED"; - - private final ProcessingEnvironment aptEnv; - - TypeHandlerBase(ProcessingEnvironment aptEnv) { - this.aptEnv = aptEnv; - } - - static Predicate isMine(TypeName type) { - TypeName withoutGenerics = type.genericTypeName(); - return info -> info.enclosingType().map(TypeName::genericTypeName).map(withoutGenerics::equals).orElse(true); - } - - // exactly one parameter - either common config, or Helidon config - static boolean hasConfigParam(TypedElementInfo info) { - List arguments = info.parameterArguments(); - if (arguments.size() != 1) { - return false; - } - TypeName argumentType = arguments.get(0).typeName(); - return CONFIG.equals(argumentType) || COMMON_CONFIG.equals(argumentType); - } - - static Element elementFor(Elements aptElements, TypedElementInfo elementInfo) { - return elementInfo.enclosingType() - .map(TypeName::fqName) - .map(aptElements::getTypeElement) - .orElse(null); - } - - /* - Method name is camel case (such as maxInitialLineLength) - result is dash separated and lower cased (such as max-initial-line-length). - Note that this same method was created in ConfigUtils in common-config, but since this - module should not have any dependencies in it a copy was left here as well. - */ - static String toConfigKey(String methodName) { - StringBuilder result = new StringBuilder(); - - char[] chars = methodName.toCharArray(); - for (char aChar : chars) { - if (Character.isUpperCase(aChar)) { - if (result.isEmpty()) { - result.append(Character.toLowerCase(aChar)); - } else { - result.append('-') - .append(Character.toLowerCase(aChar)); - } - } else { - result.append(aChar); - } - } - - return result.toString(); - } - - static String javadoc(String docComment) { - return Javadoc.parse(docComment); - } - - String key(TypedElementInfo elementInfo, ConfiguredOptionData configuredOption) { - String name = configuredOption.name(); - if (name == null || name.isBlank()) { - return toConfigKey(elementInfo.elementName()); - } - return name; - } - - String description(TypedElementInfo elementInfo, ConfiguredOptionData configuredOption) { - String desc = configuredOption.description(); - if (desc == null) { - return javadoc(elementInfo.description().orElse(null)); - } - return desc; - } - - String defaultValue(String defaultValue) { - return UNCONFIGURED_OPTION.equals(defaultValue) ? null : defaultValue; - } - - List allowedValues(ConfiguredOptionData configuredOption, TypeName type) { - if (type.equals(configuredOption.type()) || !configuredOption.allowedValues().isEmpty()) { - // this was already processed due to an explicit type defined in the annotation - // or allowed values explicitly configured in annotation - return configuredOption.allowedValues(); - } - return allowedValues(type); - } - - Optional typeInfo(TypeName typeName, Predicate predicate) { - return TypeInfoFactory.create(aptEnv, typeName); - } - - ProcessingEnvironment aptEnv() { - return aptEnv; - } - - Messager aptMessager() { - return aptEnv.getMessager(); - } - - Elements aptElements() { - return aptEnv.getElementUtils(); - } - - List params(TypedElementInfo info) { - return info.parameterArguments() - .stream() - .map(TypedElementInfo::typeName) - .toList(); - } - - void addInterfaces(ConfiguredType type, TypeInfo typeInfo, TypeName requiredAnnotation) { - for (TypeInfo interfaceInfo : typeInfo.interfaceTypeInfo()) { - if (interfaceInfo.hasAnnotation(requiredAnnotation)) { - type.addInherited(interfaceInfo.typeName()); - } else { - addSuperClasses(type, interfaceInfo, requiredAnnotation); - } - } - } - - void addSuperClasses(ConfiguredType type, TypeInfo typeInfo, TypeName requiredAnnotation) { - Optional foundSuperType = typeInfo.superTypeInfo(); - if (foundSuperType.isEmpty()) { - return; - } - TypeInfo superClass = foundSuperType.get(); - - while (true) { - if (superClass.hasAnnotation(requiredAnnotation)) { - // we only care about the first one. This one should reference its superclass/interfaces - // if they are configured as well - type.addInherited(superClass.typeName()); - return; - } - - foundSuperType = superClass.superTypeInfo(); - if (foundSuperType.isEmpty()) { - return; - } - superClass = foundSuperType.get(); - } - } - - /* - If the type is an enum that is accessible to us, provide its element, otherwise empty - */ - Optional toEnum(TypeName type) { - TypeElement typeElement = aptElements().getTypeElement(type.fqName()); - if (typeElement == null) { - return Optional.empty(); - } - if (typeElement.getKind() != ElementKind.ENUM) { - return Optional.empty(); - } - - return Optional.of(typeElement); - } - - List allowedValuesEnum(ConfiguredOptionData data, TypeElement typeElement) { - if (data.allowedValues().isEmpty()) { - // this was already processed due to an explicit type defined in the annotation - // or allowed values explicitly configured in annotation - return data.allowedValues(); - } - return allowedValuesEnum(typeElement); - } - - private List allowedValuesEnum(TypeElement typeElement) { - return typeElement.getEnclosedElements() - .stream() - .filter(element -> element.getKind().equals(ElementKind.ENUM_CONSTANT)) - .map(element -> new ConfiguredOptionData.AllowedValue(element.toString(), - javadoc(aptElements().getDocComment(element)))) - .toList(); - } - - private List allowedValues(TypeName type) { - TypeElement typeElement = aptElements().getTypeElement(type.fqName()); - if (typeElement != null && typeElement.getKind() == ElementKind.ENUM) { - return allowedValuesEnum(typeElement); - } - return List.of(); - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerMetaApi.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerMetaApi.java deleted file mode 100644 index 3da5d9db87f..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerMetaApi.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; - -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic; - -import io.helidon.common.processor.ElementInfoPredicates; -import io.helidon.common.types.TypeInfo; -import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypeNames; -import io.helidon.common.types.TypeValues; -import io.helidon.common.types.TypedElementInfo; -import io.helidon.config.metadata.processor.ConfiguredType.ProducerMethod; - -import static io.helidon.config.metadata.processor.UsedTypes.COMMON_CONFIG; -import static io.helidon.config.metadata.processor.UsedTypes.CONFIG; -import static io.helidon.config.metadata.processor.UsedTypes.META_CONFIGURED; -import static io.helidon.config.metadata.processor.UsedTypes.META_OPTION; -import static io.helidon.config.metadata.processor.UsedTypes.META_OPTIONS; - -/* - * Takes care of blueprints annotated with builder API only. - */ -class TypeHandlerMetaApi extends TypeHandlerMetaApiBase implements TypeHandler { - private final TypeInfo typeInfo; - private final TypeName typeName; - - TypeHandlerMetaApi(ProcessingEnvironment aptEnv, TypeInfo typeInfo) { - super(aptEnv); - - this.typeInfo = typeInfo; - this.typeName = typeInfo.typeName(); - } - - @Override - public TypeHandlerResult handle() { - TypeInfo targetType; - boolean isBuilder; - String module; - Optional foundTarget = findBuilderTarget(new HashSet<>(), typeInfo); - ConfiguredAnnotation configured = ConfiguredAnnotation.createMeta(typeInfo.annotation(META_CONFIGURED)); - if (!configured.ignoreBuildMethod() - && !typeInfo.modifiers().contains(TypeValues.MODIFIER_ABSTRACT) - && foundTarget.isPresent()) { - // this is a builder, we need the target type Builder - TypeName targetTypeName = foundTarget.get(); - targetType = typeInfo(targetTypeName, ElementInfoPredicates::isMethod) - .orElseThrow(() -> new IllegalStateException("Cannot find target type info for type " - + targetTypeName.fqName() - + ", discovered for type: " + typeInfo.typeName().fqName())); - isBuilder = true; - module = targetType.module().orElse("unknown"); - } else { - targetType = typeInfo; - isBuilder = false; - module = typeInfo.module().orElse("unknown"); - } - - /* - now we know whether this is - - a builder + known target class (result of builder() method) - - a standalone class (probably with public static create(Config) method) - - an interface/abstract class only used for inheritance - */ - ConfiguredType type = new ConfiguredType(configured, - typeName, - targetType.typeName(), - false); - - /* - we also need to know all superclasses / interfaces that are configurable so we can reference them - these may be from other modules, so we cannot create a single set of values from all types - */ - addSuperClasses(type, typeInfo, META_CONFIGURED); - addInterfaces(type, typeInfo, META_CONFIGURED); - - if (isBuilder) { - // builder - processBuilderType(typeInfo, type, typeName, targetType); - } else { - // standalone class with create method(s), or interface/abstract class - processTargetType(typeInfo, type, typeName, type.standalone()); - } - - return new TypeHandlerResult(targetType.typeName(), module, type); - } - - // annotated type or type methods (not a builder) - private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName typeName, boolean standalone) { - // go through all methods, find all create methods and create appropriate configured producers for them - // if there is a builder, add the builder producer as well - - List methods = typeInfo.elementInfo() - .stream() - .filter(ElementInfoPredicates::isMethod) - // public, package local or protected - .filter(Predicate.not(ElementInfoPredicates::isPrivate)) - // static - .filter(ElementInfoPredicates::isStatic) - .toList(); - - // either this is a target class (such as an interface with create method) - // or this is an interface/abstract class inherited by builders - boolean isTargetType = false; - List validMethods = new LinkedList<>(); - TypedElementInfo configCreator = null; - - // now we have just public static methods, let's look for create/builder - for (TypedElementInfo method : methods) { - String name = method.elementName(); - - if ("create".equals(name)) { - if (method.typeName().genericTypeName().equals(typeName.genericTypeName())) { - validMethods.add(method); - List parameters = method.parameterArguments(); - if (parameters.size() == 1) { - TypeName paramType = parameters.get(0).typeName(); - if (paramType.equals(CONFIG) || paramType.equals(COMMON_CONFIG)) { - configCreator = method; - } - } - isTargetType = true; - } - } else if (name.equals("builder")) { - aptMessager().printMessage(Diagnostic.Kind.ERROR, "Type " + typeName.fqName() + " is marked with @Configured" - + ", yet it has a static builder() method. Please mark the builder instead " - + "of this class.", - aptElements().getTypeElement(typeName.fqName())); - } - } - - if (isTargetType) { - if (configCreator != null) { - type.addProducer(new ProducerMethod(true, - typeName, - configCreator.elementName(), - params(configCreator))); - } - - // now let's find all methods with @ConfiguredOption - for (TypedElementInfo validMethod : validMethods) { - List options = findConfiguredOptionAnnotations(validMethod); - - if (options.isEmpty()) { - continue; - } - - for (ConfiguredOptionData data : options) { - if ((data.name() == null || data.name().isBlank()) && !data.merge()) { - TypeElement typeElement = aptElements().getTypeElement(typeName.fqName()); - aptMessager().printMessage(Diagnostic.Kind.ERROR, - "ConfiguredOption on " + typeElement + "." - + validMethod - + " does not have value defined. It is mandatory on non-builder " - + "methods", - typeElement); - return; - } - - if (data.description() == null || data.description().isBlank()) { - TypeElement typeElement = aptElements().getTypeElement(typeName.fqName()); - aptMessager().printMessage(Diagnostic.Kind.ERROR, - "ConfiguredOption on " + typeElement + "." + validMethod - + " does not have description defined. It is mandatory on non-builder " - + "methods", - typeElement); - return; - } - - if (data.type() == null) { - // this is the default value - data.type(TypeNames.STRING); - } - - ConfiguredType.ConfiguredProperty prop = new ConfiguredType.ConfiguredProperty(null, - data.name(), - data.description(), - data.defaultValue(), - data.type(), - data.experimental(), - data.optional(), - data.kind(), - data.provider(), - data.providerType(), - data.deprecated(), - data.merge(), - data.allowedValues()); - type.addProperty(prop); - } - } - } else { - // this must be a class/interface used by other classes to extend, so we care about all builder style - // methods - if (standalone) { - aptMessager().printMessage(Diagnostic.Kind.ERROR, - "Type " + typeName.fqName() + " is marked as standalone configuration unit, " - + "yet it does have " - + "neither a builder method, nor a create method"); - return; - } - - typeInfo.elementInfo() - .stream() - .filter(ElementInfoPredicates::isMethod) // methods - .filter(Predicate.not(ElementInfoPredicates::isPrivate)) // public, package or protected - .filter(Predicate.not(ElementInfoPredicates::isStatic)) // not static - .filter(TypeHandlerMetaApiBase.isMine(typeName)) // declared on this type - .forEach(it -> processBuilderMethod(typeName, type, it)); - } - } - - // annotated builder methods - private void processBuilderType(TypeInfo typeInfo, ConfiguredType type, TypeName typeName, TypeInfo targetType) { - type.addProducer(new ProducerMethod(false, typeName, "build", List.of())); - - TypeName targetTypeName = targetType.typeName(); - // check if static TargetType create(Config) exists - if (targetType.elementInfo() - .stream() - .filter(ElementInfoPredicates::isMethod) - .filter(ElementInfoPredicates::isStatic) - .filter(Predicate.not(ElementInfoPredicates::isPrivate)) - .filter(ElementInfoPredicates.elementName("create")) - .filter(TypeHandlerMetaApiBase::hasConfigParam) - .anyMatch(TypeHandlerMetaApiBase.isMine(targetTypeName))) { - - type.addProducer(new ProducerMethod(true, - targetTypeName, - "create", - List.of(COMMON_CONFIG))); - } - - // find all public methods annotated with @ConfiguredOption - typeInfo.elementInfo() - .stream() - .filter(ElementInfoPredicates::isMethod) // methods - .filter(Predicate.not(ElementInfoPredicates::isPrivate)) // not private - .filter(TypeHandlerMetaApiBase.isMine(typeName)) // declared on this type - .filter(it -> it.hasAnnotation(META_OPTION) || it.hasAnnotation(META_OPTIONS)) - .forEach(it -> processBuilderMethod(typeName, type, it)); - } - - private List builderMethodParams(TypedElementInfo elementInfo, OptionType type) { - return params(elementInfo); - } - - private void processBuilderMethod(TypeName typeName, ConfiguredType configuredType, TypedElementInfo elementInfo) { - processBuilderMethod(typeName, configuredType, elementInfo, this::optionType, this::builderMethodParams); - } - - private OptionType optionType(TypedElementInfo elementInfo, ConfiguredOptionData annotation) { - if (annotation.type() == null || annotation.type().equals(META_OPTION)) { - // guess from method - - List parameters = elementInfo.parameterArguments(); - if (parameters.size() != 1) { - aptMessager().printMessage(Diagnostic.Kind.ERROR, "Method " + elementInfo.elementName() - + " is annotated with @ConfiguredOption, " - + "yet it does not have explicit type, or exactly one parameter", - aptElements().getTypeElement(elementInfo.enclosingType().map(TypeName::fqName) - .orElse(null))); - return new OptionType(TypeNames.STRING, "VALUE"); - } else { - TypedElementInfo parameter = parameters.iterator().next(); - TypeName paramType = parameter.typeName(); - - if (paramType.isList() || paramType.isSet()) { - return new OptionType(paramType.typeArguments().get(0), "LIST"); - } - - if (paramType.isMap()) { - return new OptionType(paramType.typeArguments().get(1), "MAP"); - } - - return new OptionType(paramType.boxed(), annotation.kind()); - } - - } else { - // use the one defined on annotation - return new OptionType(annotation.type(), annotation.kind()); - } - } - - private Optional findBuilderTarget(Set processed, TypeInfo typeInfo) { - // non-private build method exists that has no parameters, not static, and returns a type - return typeInfo.elementInfo() - .stream() - .filter(ElementInfoPredicates::isMethod) - .filter(Predicate.not(ElementInfoPredicates::isStatic)) - .filter(ElementInfoPredicates::hasNoArgs) - .filter(ElementInfoPredicates.elementName("build")) - .filter(Predicate.not(ElementInfoPredicates::isVoid)) - .filter(Predicate.not(ElementInfoPredicates::isPrivate)) - .findFirst() - .map(it -> it.typeName()); - } -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerResult.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerResult.java deleted file mode 100644 index 213d81b8632..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/TypeHandlerResult.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config.metadata.processor; - -import io.helidon.common.types.TypeName; - -/** - * Result of annotation processing. - * - * @param targetType type that is configured (result of the builder, runtime type of a prototype) - * @param moduleName module of the type - * @param configuredType collected configuration metadata - */ -record TypeHandlerResult(TypeName targetType, - String moduleName, - ConfiguredType configuredType) { -} diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/package-info.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/package-info.java deleted file mode 100644 index 50460f37d7c..00000000000 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Annotation processor generating JSON metadata for configuration. - */ -package io.helidon.config.metadata.processor; diff --git a/config/metadata-processor/src/main/java/module-info.java b/config/metadata-processor/src/main/java/module-info.java deleted file mode 100644 index e12f1089df0..00000000000 --- a/config/metadata-processor/src/main/java/module-info.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Annotation processor generating JSON metadata for configuration. - */ -module io.helidon.config.metadata.processor { - - requires java.compiler; - requires io.helidon.common.types; - requires io.helidon.common.processor; - - exports io.helidon.config.metadata.processor; - - provides javax.annotation.processing.Processor with io.helidon.config.metadata.processor.ConfigMetadataProcessor; - -} \ No newline at end of file diff --git a/config/metadata/README.md b/config/metadata/README.md new file mode 100644 index 00000000000..7f7ef77e091 --- /dev/null +++ b/config/metadata/README.md @@ -0,0 +1,304 @@ +# Configuration metadata + +Configuration metadata can be obtained from annotations, either of generated builders using `@Prototype.Configured` and `Option.Configured` (defined on `Blueprint` + interfaces), or from annotated builders and types using `@io.helidon.config.metadata.Configured` and related. + +Blueprints MUST be annotated with annotations from `io.helidon.builder.api` package (Config metadata annotations will most likely fail on these types). +Other types may be annotated with annotations from `io.helidon.config.metadata` package (Builder API annotations are not supported outside of `Blueprint` interfaces). + +The following modules for handling config metadata exist: + +- `io.helidon.config:helidon-config-metadata`: annotations to add to non-Blueprint types to generate documentation +- `io.helidon.config.metadata:helidon-config-metadata-codegen`: code generator that reads annotations and creates `config-metadata.json` +- `io.helidon.config:helidon-config-metadata-processor`: deprecated processor that does the same as `codegen` above +- `io.helidon.config.metadata:helidon-config-metadata-docs`: code generator that reads `config.metadata.json` and generates Helidon `.adoc` files that are part of Helidon Config reference documentation + +How to handle each task is described below. + +## Add metadata to a configurable component + +In both cases, you need to add the annotation processor for codegen, and the Config metadata codegen modules to the compiler plugin annotation processor path. +If this is done in Helidon itself (in this repository), the same must be added to compiler plugin dependencies, to correctly organize the reactor (as annotation processor path does not add dependencies that the reactor can see). + +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + + + +``` + +### Helidon Builder (Blueprints) + +1. Annotate the `Blueprint` interface with `Prototype.Configured` +2. Annotate configurable methods with `Option.Configured` + +The generated JSON will honor types returned by the option methods, generate configuration keys from the method name (e.g. method `clientAuth` would have configuration key `client-auth`), honor default values, and use method javadocs as a source for description of the option. + +See Javadoc of the annotations to see additional customization options. + +### Helidon Config Metadata (Other types) + +Any type except for `Blueprint` can be annotated with `io.helidon.config.metadata.Configured`, and then options may be added to methods (or to the type itself). +This approach can do less, because it does not always have methods to analyze, but it is attempting to do the similar to what is written above for the Builder API (e.g. if a manually constructed builder setter method is annotated, it will still use the correct type, javadoc etc.). + +To add meta configuration: + +1. Add the following dependency to your `pom.xml` (optional dependency, as it only defines annotations): + ```xml + + io.helidon.config + helidon-config-metadata + true + + ``` +2. Update the `module-info.java` by adding + ```java + requires static io.helidon.config.metadata; + ``` +3. Annotate the configured class using `@Configured` - usually the builder class. If there is only a factory method, annotate the class containing the factory method +4. Annotate builder methods using `@ConfiguredOption` - the type of the parameter will be used as type of the property, provides full customization using annotation properties +5. In case a factory method is the only one available, annotate it with repeating `@ConfiguredOption` to list all annotations +6. Look at existing examples if in doubt +7. Check the output in `target/classes/META-INF/helidon` to see what was generated + +## Generate Config Reference Documentation + +There are two ways to do this: + +1. go to the `config/metadata/docs` directory, and run `mvn compile exec:exec` +2. go to the `docs` directory, and run `mvn package -Pconfigdoc` + +In both cases, all dependencies from `all/pom.xml` except for `helidon-logging-log4j` and `helidon-logging-slf4j` are analyzed, and documentation is updated in `docs/src/main/asciidoc/config`. +Please check the output of, as there may be warnings that point out a possible problem (such as a file that exists, but is not backed by any `config-metadata.json`). + +## Output file format + +The file is `META-INF/helidon/config-metadata.json` + +### Root Element +Root of the file is an array of module objects + +```json +[ + { + "module": "" + } +] +``` + +### Module element +Module is equivalent to `module-info.java`. This approach allows merging of multiple metadata files into a single +file. + +```json +{ +"module": "module name (from module-info.java)", +"types" : [] +} +``` + +### Type element +Each type represents a configurable unit. + +"type": +```json +{ + "type": "fully qualified type name of the configured class (the class built by builder, or created by factory method)", + "standalone": true, + "prefix": "server", + "description": "Documentation of this type", + "producers": ["methods"], + "inherits": ["fully qualified type names of superclasses/interfaces this type extends/implements"], + "provides": ["fully qualified type names of provided services"], + "options": [] +} +``` + +* prefix - configuration prefix, if standalone, this is the root configuration key (such as `server`, `tracing`), + if nested, this is a prefix added before all options of this type (such as each Security provider) +* standalone - if set to true, this type is configurable in the root of configuration (such as webserver, security, tracing, metrics), + otherwise it is a nested type in another configuration (such as WebServerTls, OidcConfig) + default is `false` (if not defined in document) +* producers (methods in general) - format is "fully qualified type name#method name (method parameter types)", example: + `io.helidon.security.providers.common.OutboundTarget.Builder#build()` + +### Option element +Each option is one key in configuration. Option can either be a simple value (String, Long, URI), or a complex value +defined by another type (nested types). + +```json +{ + "key": "key in configuration (may be missing, if this merges with parent)", + "type": "fully qualified type of the configuration option, defaults to java.lang.String", + "description": "description of this configuration node (expected to be non-empty)", + "kind": "LIST|MAP|VALUE", + "method": "annotated method", + "merge": true, + "experimental": true, + "required": true, + "provider": true, + "deprecated": true, + "defaultValue": "string default value (only for value nodes)" + "allowedValues": [] +} +``` + +* key - there is a special case when we need to define a list on a method that does not use a nested type + in such a case, the key contains `*` to mark a list. Example: `secrets.*.provider`, `secrets.*.config` + would result in: + ```yaml + secrets: + - provider: "provider" + config: "something" + ``` +* type - either a type directly mapped to an object (String, Integer, URI etc.), or a nested type defined either in + this or in another module. Nested configuration may be in another metadata json. + * `Kind.LIST` - this is a list of values (either simple values, or objects) + * `Kind.MAP` - this is a map of values, using keys of the map as keys, and values of the map as values, `type` defines + the value type (must be a simple value), key is expected to be `java.lang.String` + * `Kind.VALUE` - either a simple value (String, Long etc.) or a nested object (this is the default) +* method - method to be called to configure this option (may be a static factory method, such as `create(Config)`, or a builder method) +* merge - if set to `true` (default is `false`) this option's key is ignored and all its nested keys are inserted + directly into the parent option +* experimental - experimental options may change without warning even between minor releases (such as support for Loom in Java) +* required - if set to `true` (default is `false`) this option must be present in configuration if parent option is present +* provider - this option expects a type with matching `provides` - such as security providers configuration +* deprecated - this option should no longer be used, there is another option allowing the same (usually when renaming options, + fixing inconsistencies etc.); description should provide more details +* defaultValue - for simple values, this defines the string default value + +Example of a simple optional option with String type: +```json +{ + "key": "default-authorization-provider", + "description": "ID of the default authorization provider", + "method": "io.helidon.security.Security.Builder#authorizationProvider(io.helidon.security.spi.AuthorizationProvider)" +} +``` + +Example of an option with explicit type and default value: +```json +{ + "key": "enabled", + "type": "java.lang.Boolean", + "description": "Security can be disabled using configuration, or explicitly.\n By default, security instance is enabled.\n Disabled security instance will not perform any checks and allow\n all requests.", + "defaultValue": "true", + "method": "io.helidon.security.Security.Builder#enabled(boolean)" +} +``` + +Example of a nested configuration option: +```json +{ + "key": "environment.server-time", + "type": "io.helidon.security.SecurityTime", + "description": "Server time to use when evaluating security policies that depend on time.", + "method": "io.helidon.security.Security.Builder#serverTime(io.helidon.security.SecurityTime)" +} +``` + +### AllowedValue element +Allowed value defines a fixed set of allowed values for a configuration option. Note that allowed values for `enum` types are automatically generated as long as the enum is in the same module (so we can read its javadoc), otherwise the allowed value is still there, but the description is not provided. + +```json +{ + "value": "a permissible value", + "description": "description of the value, may be empty if this is a generated value from enum that is not part of this module" +} +``` + +Example of an option with allowed values: +```json +{ + "key": "provider-policy.type", + "type": "io.helidon.security.ProviderSelectionPolicyType", + "description": "Type of the policy.", + "defaultValue": "FIRST", + "method": "io.helidon.security.Security.Builder#providerSelectionPolicy(java.util.function.Function)", + "allowedValues": [ + { + "value": "FIRST", + "description": "Choose first provider from the list by default.\n Choose provider with the name defined when explicit provider requested." + }, + { + "value": "COMPOSITE", + "description": "Can compose multiple providers together to form a single\n logical provider." + }, + { + "value": "CLASS", + "description": "Explicit class for a custom ProviderSelectionPolicyType." + } + ] +} +``` + +### AllowedValue element +Allowed value defines a fixed set of allowed values for a configuration option. + +```json +{ + "value": "a permissible value", + "description": "description of the value, may be empty if this is a generated value from enum that is not part of this module" +} +``` + +Example of an option with allowed values: +```json +{ + "key": "provider-policy.type", + "type": "io.helidon.security.ProviderSelectionPolicyType", + "description": "Type of the policy.", + "defaultValue": "FIRST", + "method": "io.helidon.security.Security.Builder#providerSelectionPolicy(java.util.function.Function)", + "allowedValues": [ + { + "value": "FIRST", + "description": "Choose first provider from the list by default.\n Choose provider with the name defined when explicit provider requested." + }, + { + "value": "COMPOSITE", + "description": "Can compose multiple providers together to form a single\n logical provider." + }, + { + "value": "CLASS", + "description": "Explicit class for a custom ProviderSelectionPolicyType." + } + ] +} +``` \ No newline at end of file diff --git a/config/metadata/codegen/pom.xml b/config/metadata/codegen/pom.xml new file mode 100644 index 00000000000..d12cb4538b2 --- /dev/null +++ b/config/metadata/codegen/pom.xml @@ -0,0 +1,62 @@ + + + + + 4.0.0 + + io.helidon.config + helidon-config-metadata-project + 4.2.0-SNAPSHOT + + + io.helidon.config.metadata + helidon-config-metadata-codegen + Helidon Config Metadata Codegen + + + Configuration Metadata Code Generation + + + + + io.helidon.common + helidon-common-types + + + io.helidon.codegen + helidon-codegen + + + io.helidon.metadata + helidon-metadata-hson + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java new file mode 100644 index 00000000000..47e4bf2f96b --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.RoundContext; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.metadata.hson.Hson; + +class ConfigMetadataCodegenExtension implements CodegenExtension { + /* + * Configuration metadata file location. + */ + private static final String META_FILE = "META-INF/helidon/config-metadata.json"; + + private final Set blueprints = new HashSet<>(); + private final Set configMetadata = new HashSet<>(); + // Newly created options as part of this processor run - these will be stored to META_FILE + // map of type name to its configured type + private final Map newOptions = new HashMap<>(); + // map of module name to list of classes that belong to it + private final Map> moduleTypes = new HashMap<>(); + + private final CodegenContext ctx; + + ConfigMetadataCodegenExtension(CodegenContext ctx) { + this.ctx = ctx; + } + + @Override + public void process(RoundContext roundContext) { + // we may have multiple rounds, let's collect what we can + // the type info may change (i.e. we code generate something that is not available in the + // first round) + roundContext.annotatedTypes(ConfigMetadataTypes.CONFIGURED) + .forEach(it -> blueprints.add(it.typeName())); + + roundContext.annotatedTypes(ConfigMetadataTypes.META_CONFIGURED) + .forEach(it -> configMetadata.add(it.typeName())); + } + + @Override + public void processingOver(RoundContext roundContext) { + Stream.concat(typesToProcess(blueprints) + .map(it -> TypeHandlerBuilderApi.create(ctx, it)), + typesToProcess(configMetadata) + .map(it -> TypeHandlerMetaApi.create(ctx, it))) + .map(TypeHandler::handle) + .forEach(it -> { + TypeName targetType = it.targetType(); + newOptions.put(targetType, it.configuredType()); + moduleTypes.computeIfAbsent(it.moduleName(), + ignored -> new ArrayList<>()) + .add(targetType); + }); + + storeMetadata(); + } + + private Stream typesToProcess(Set typeNames) { + return typeNames.stream() + .map(ctx::typeInfo) + .flatMap(Optional::stream); + } + + private void storeMetadata() { + if (moduleTypes.isEmpty()) { + // only store if anything is available + return; + } + List root = new ArrayList<>(); + + for (var module : moduleTypes.entrySet()) { + String moduleName = module.getKey(); + var types = module.getValue(); + List typeArray = new ArrayList<>(); + types.forEach(it -> newOptions.get(it).write(typeArray)); + root.add(Hson.structBuilder() + .set("module", moduleName) + .setStructs("types", typeArray) + .build()); + } + + if (!root.isEmpty()) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintWriter w = new PrintWriter(baos, true, StandardCharsets.UTF_8)) { + Hson.Array.create(root).write(w); + } + ctx.filer().writeResource(baos.toByteArray(), META_FILE); + } + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenProvider.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenProvider.java new file mode 100644 index 00000000000..10b41e18717 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.Set; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.codegen.spi.CodegenExtensionProvider; +import io.helidon.common.types.TypeName; + +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.CONFIGURED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.META_CONFIGURED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.META_OPTION; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.META_OPTIONS; + +/** + * A Java {@link java.util.ServiceLoader} service implementation to add config metadata code generation. + */ +public class ConfigMetadataCodegenProvider implements CodegenExtensionProvider { + /** + * Public constructor required by {@link java.util.ServiceLoader}. + */ + public ConfigMetadataCodegenProvider() { + } + + @Override + public CodegenExtension create(CodegenContext ctx, TypeName generatorType) { + return new ConfigMetadataCodegenExtension(ctx); + } + + @Override + public Set supportedAnnotations() { + return Set.of(META_CONFIGURED, + META_OPTION, + META_OPTIONS, + CONFIGURED); + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataTypes.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataTypes.java new file mode 100644 index 00000000000..76a3939b200 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataTypes.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import io.helidon.common.types.TypeName; + +final class ConfigMetadataTypes { + static final TypeName DEPRECATED = TypeName.create(Deprecated.class); + /* + Using config metadata + */ + static final TypeName META_CONFIGURED = TypeName.create("io.helidon.config.metadata.Configured"); + static final TypeName META_OPTION = TypeName.create("io.helidon.config.metadata.ConfiguredOption"); + static final TypeName META_OPTIONS = TypeName.create("io.helidon.config.metadata.ConfiguredOptions"); + + /* + Using builder API + */ + static final TypeName COMMON_CONFIG = TypeName.create("io.helidon.common.config.Config"); + static final TypeName CONFIG = TypeName.create("io.helidon.config.Config"); + static final TypeName PROTOTYPE_FACTORY = TypeName.create("io.helidon.builder.api.Prototype.Factory"); + static final TypeName BLUEPRINT = TypeName.create("io.helidon.builder.api.Prototype.Blueprint"); + static final TypeName CONFIGURED = TypeName.create("io.helidon.builder.api.Prototype.Configured"); + static final TypeName PROTOTYPE_PROVIDES = TypeName.create("io.helidon.builder.api.Prototype.Provides"); + static final TypeName DESCRIPTION = TypeName.create("io.helidon.builder.api.Description"); + static final TypeName OPTION_CONFIGURED = TypeName.create("io.helidon.builder.api.Option.Configured"); + static final TypeName OPTION_REQUIRED = TypeName.create("io.helidon.builder.api.Option.Required"); + static final TypeName OPTION_PROVIDER = TypeName.create("io.helidon.builder.api.Option.Provider"); + static final TypeName OPTION_ALLOWED_VALUES = TypeName.create("io.helidon.builder.api.Option.AllowedValues"); + static final TypeName OPTION_ALLOWED_VALUE = TypeName.create("io.helidon.builder.api.Option.AllowedValue"); + static final TypeName OPTION_DEFAULT = TypeName.create("io.helidon.builder.api.Option.Default"); + static final TypeName OPTION_DEFAULT_INT = TypeName.create("io.helidon.builder.api.Option.DefaultInt"); + static final TypeName OPTION_DEFAULT_DOUBLE = TypeName.create("io.helidon.builder.api.Option.DefaultDouble"); + static final TypeName OPTION_DEFAULT_BOOLEAN = TypeName.create("io.helidon.builder.api.Option.DefaultBoolean"); + static final TypeName OPTION_DEFAULT_LONG = TypeName.create("io.helidon.builder.api.Option.DefaultLong"); + static final TypeName OPTION_DEFAULT_METHOD = TypeName.create("io.helidon.builder.api.Option.DefaultMethod"); + static final TypeName OPTION_DEFAULT_CODE = TypeName.create("io.helidon.builder.api.Option.DefaultCode"); + + private ConfigMetadataTypes() { + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredAnnotation.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredAnnotation.java new file mode 100644 index 00000000000..bbc472decc6 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredAnnotation.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; + +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.CONFIGURED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.DESCRIPTION; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.PROTOTYPE_PROVIDES; + +record ConfiguredAnnotation(Optional description, + Optional prefix, + List provides, + boolean root, + boolean ignoreBuildMethod) { + + static ConfiguredAnnotation createMeta(Annotation annotation) { + return new ConfiguredAnnotation( + annotation.stringValue("description").filter(Predicate.not(String::isBlank)), + annotation.stringValue("prefix").filter(Predicate.not(String::isBlank)), + toProvidesMeta(annotation), + annotation.booleanValue("root").orElse(false), + annotation.booleanValue("ignoreBuildMethod").orElse(false) + ); + } + + static ConfiguredAnnotation createBuilder(TypeInfo blueprint) { + Optional config = blueprint.findAnnotation(CONFIGURED) + .flatMap(Annotation::stringValue) + .filter(Predicate.not(String::isBlank)); + boolean isRoot = config.isPresent() && blueprint.findAnnotation(CONFIGURED) + .flatMap(it -> it.booleanValue("root")) + .orElse(true); + + return new ConfiguredAnnotation( + blueprint.findAnnotation(DESCRIPTION).flatMap(Annotation::stringValue), + config, + toProvidesBuilder(blueprint), + isRoot, + false + ); + } + + private static List toProvidesBuilder(TypeInfo blueprint) { + return blueprint.findAnnotation(PROTOTYPE_PROVIDES) + .flatMap(Annotation::stringValues) + .stream() + .flatMap(List::stream) + .toList(); + } + + private static List toProvidesMeta(Annotation annotation) { + return annotation.stringValues("provides") + .orElseGet(List::of); + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredOptionData.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredOptionData.java new file mode 100644 index 00000000000..9771b8b3702 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredOptionData.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import io.helidon.codegen.CodegenContext; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.common.types.ElementKind.ENUM; +import static io.helidon.common.types.ElementKind.ENUM_CONSTANT; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.DEPRECATED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.DESCRIPTION; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_ALLOWED_VALUE; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_ALLOWED_VALUES; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_CONFIGURED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_DEFAULT; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_DEFAULT_BOOLEAN; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_DEFAULT_CODE; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_DEFAULT_DOUBLE; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_DEFAULT_INT; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_DEFAULT_LONG; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_DEFAULT_METHOD; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_PROVIDER; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_REQUIRED; +import static io.helidon.config.metadata.codegen.TypeHandlerBase.UNCONFIGURED_OPTION; +import static java.util.function.Predicate.not; + +final class ConfiguredOptionData { + private final List allowedValues = new LinkedList<>(); + + private boolean configured = true; + private String name; + private TypeName type; + private String description; + private boolean required; + private String defaultValue; + private boolean experimental; + private boolean provider; + private boolean deprecated; + private boolean merge; + private String kind = "VALUE"; + private TypeName providerType; + + // create from @ConfiguredOption in config-metadata + static ConfiguredOptionData createMeta(CodegenContext ctx, Annotation option) { + ConfiguredOptionData result = new ConfiguredOptionData(); + + option.booleanValue("configured").ifPresent(result::configured); + option.stringValue("key").filter(not(String::isBlank)).ifPresent(result::name); + option.stringValue("description").filter(not(String::isBlank)).ifPresent(result::description); + option.stringValue().filter(not(UNCONFIGURED_OPTION::equals)).ifPresent(result::defaultValue); + option.booleanValue("experimental").ifPresent(result::experimental); + option.booleanValue("required").ifPresent(result::required); + option.booleanValue("mergeWithParent").ifPresent(result::merge); + option.typeValue("type").ifPresent(result::type); + option.stringValue("kind").ifPresent(result::kind); + option.booleanValue("provider").ifPresent(result::provider); + option.booleanValue("deprecated").ifPresent(result::deprecated); + option.annotationValues("allowedValues") + .or(() -> option.annotationValue("allowedValue").map(List::of)) + .stream() + .flatMap(List::stream) + .map(AllowedValue::create) + .forEach(result::addAllowedValue); + + if (result.allowedValues.isEmpty()) { + // if enum, fill this in + option.getValue("type") + .map(TypeName::create) + .flatMap(ctx::typeInfo) + .filter(it -> it.kind() == ENUM) + .ifPresent(it -> enumAllowedValues(result.allowedValues(), it)); + } + + return result; + } + + // create from Option annotations in builder-api + static ConfiguredOptionData createBuilder(TypedElementInfo element) { + ConfiguredOptionData result = new ConfiguredOptionData(); + + Optional optionConfigured = element.findAnnotation(OPTION_CONFIGURED); + optionConfigured.flatMap(Annotation::stringValue).filter(not(String::isBlank)) + .ifPresent(result::name); + optionConfigured.flatMap(it -> it.booleanValue("merge")) + .ifPresent(result::merge); + + element.findAnnotation(DESCRIPTION).flatMap(Annotation::stringValue).ifPresent(result::description); + element.findAnnotation(OPTION_REQUIRED).ifPresent(it -> result.required(true)); + element.findAnnotation(OPTION_PROVIDER).ifPresent(it -> { + it.typeValue().ifPresent(result::providerType); + result.provider(true); + }); + element.findAnnotation(DEPRECATED).ifPresent(it -> result.deprecated(true)); + element.findAnnotation(OPTION_ALLOWED_VALUES) + .flatMap(Annotation::annotationValues) + .or(() -> element.findAnnotation(OPTION_ALLOWED_VALUE).map(List::of)) + .stream() + .flatMap(List::stream) + .map(AllowedValue::create) + .forEach(result::addAllowedValue); + + Optional defaultValues = element.findAnnotation(OPTION_DEFAULT) + .or(() -> element.findAnnotation(OPTION_DEFAULT_INT)) + .or(() -> element.findAnnotation(OPTION_DEFAULT_BOOLEAN)) + .or(() -> element.findAnnotation(OPTION_DEFAULT_LONG)) + .or(() -> element.findAnnotation(OPTION_DEFAULT_DOUBLE)); + if (defaultValues.isPresent()) { + List strings = defaultValues.get().stringValues().orElseGet(List::of); + result.defaultValue(String.join(", ", strings)); + } else if (element.hasAnnotation(OPTION_DEFAULT_METHOD)) { + Annotation annotation = element.annotation(OPTION_DEFAULT_METHOD); + TypeName type = annotation.typeValue("type") + .filter(Predicate.not(OPTION_DEFAULT_METHOD::equals)) + .or(element::enclosingType) + .orElse(null); + String value = annotation.stringValue().orElse(null); + if (value != null) { + // this should always be true, as it is mandatory + if (type == null) { + result.defaultValue(value + "()"); + } else { + result.defaultValue(type.fqName() + "." + value + "()"); + } + } + } else if (element.hasAnnotation(OPTION_DEFAULT_CODE)) { + element.annotation(OPTION_DEFAULT_CODE).stringValue().ifPresent(result::defaultValue); + } + + return result; + } + + static void enumAllowedValues(List allowedValues, TypeInfo typeInfo) { + typeInfo.elementInfo() + .stream() + .filter(it -> it.kind() == ENUM_CONSTANT) + .forEach(it -> { + allowedValues.add(new AllowedValue(it.elementName(), it.description() + .map(Javadoc::parse) + .orElse(""))); + }); + } + + List allowedValues() { + return allowedValues; + } + + String name() { + return name; + } + + TypeName type() { + return type; + } + + String description() { + return description; + } + + boolean optional() { + return !required; + } + + String defaultValue() { + return defaultValue; + } + + boolean experimental() { + return experimental; + } + + boolean provider() { + return provider; + } + + TypeName providerType() { + return providerType; + } + + boolean deprecated() { + return deprecated; + } + + boolean merge() { + return merge; + } + + String kind() { + return kind; + } + + boolean configured() { + return configured; + } + + void type(TypeName type) { + this.type = type; + } + + void name(String name) { + this.name = name; + } + + void description(String description) { + this.description = description; + } + + void required(boolean required) { + this.required = required; + } + + void defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + void experimental(boolean experimental) { + this.experimental = experimental; + } + + void provider(boolean provider) { + this.provider = provider; + } + + void providerType(TypeName provider) { + this.providerType = provider; + } + + void deprecated(boolean deprecated) { + this.deprecated = deprecated; + } + + void merge(boolean merge) { + this.merge = merge; + } + + void kind(String kind) { + this.kind = kind; + } + + void addAllowedValue(AllowedValue value) { + this.allowedValues.add(value); + } + + void configured(boolean configured) { + this.configured = configured; + } + + static final class AllowedValue { + private String value; + private String description; + + private AllowedValue() { + } + + AllowedValue(String value, String description) { + this.value = value; + this.description = description; + } + + String value() { + return value; + } + + String description() { + return description; + } + + private static AllowedValue create(Annotation annotation) { + AllowedValue result = new AllowedValue(); + + annotation.stringValue().ifPresent(it -> result.value = it); + annotation.stringValue("description").filter(not(String::isBlank)).ifPresent(it -> result.description = it); + + return result; + } + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredType.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredType.java new file mode 100644 index 00000000000..6801a0e28ff --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredType.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.TypeName; +import io.helidon.metadata.hson.Hson; + +final class ConfiguredType { + private final Set allProperties = new HashSet<>(); + private final List producerMethods = new LinkedList<>(); + /* + * The type that is built by a builder, or created using create method. + */ + private final TypeName targetClass; + /* + The type we are processing that has @Configured annotation + */ + private final TypeName annotatedClass; + private final List inherited = new LinkedList<>(); + private final ConfiguredAnnotation configured; + + ConfiguredType(ConfiguredAnnotation configured, TypeName annotatedClass, TypeName targetClass, boolean typeDefinition) { + this.annotatedClass = annotatedClass; + this.targetClass = targetClass; + this.configured = configured; + } + + ConfiguredType addProducer(ProducerMethod producer) { + producerMethods.add(producer); + return this; + } + + ConfiguredType addProperty(ConfiguredProperty property) { + allProperties.add(property); + return this; + } + + List producers() { + return producerMethods; + } + + Set properties() { + return allProperties; + } + + String targetClass() { + return targetClass.fqName(); + } + + String annotatedClass() { + return annotatedClass.fqName(); + } + + boolean standalone() { + return configured.root(); + } + + String prefix() { + return configured.prefix().orElse(null); + } + + void write(List typeArray) { + var typeObject = Hson.Struct.builder(); + + typeObject.set("type", targetClass()); + typeObject.set("annotatedType", annotatedClass()); + if (standalone()) { + typeObject.set("standalone", true); + } + configured.prefix().ifPresent(it -> typeObject.set("prefix", it)); + configured.description().ifPresent(it -> typeObject.set("description", it)); + + if (!inherited.isEmpty()) { + typeObject.setStrings("inherits", inherited.stream() + .map(TypeName::fqName) + .toList()); + } + + if (!configured.provides().isEmpty()) { + typeObject.setStrings("provides", configured.provides()); + } + + if (!producerMethods.isEmpty()) { + typeObject.setStrings("producers", producerMethods.stream() + .map(Object::toString) + .collect(Collectors.toList())); + } + + List options = new ArrayList<>(); + for (ConfiguredProperty property : allProperties) { + writeProperty(options, "", property); + } + typeObject.setStructs("options", options); + + typeArray.add(typeObject.build()); + } + + @Override + public String toString() { + return targetClass.fqName(); + } + + void addInherited(TypeName classOrIface) { + inherited.add(classOrIface); + } + + private static String paramsToString(List params) { + return params.stream() + .map(TypeName::resolvedName) + .collect(Collectors.joining(", ")); + } + + private void writeProperty(List optionsBuilder, + String prefix, + ConfiguredProperty property) { + + var optionBuilder = Hson.Struct.builder(); + if (property.key() != null && !property.key.isBlank()) { + optionBuilder.set("key", prefix(prefix, property.key())); + } + if (!"java.lang.String".equals(property.type)) { + optionBuilder.set("type", property.type()); + } + optionBuilder.set("description", property.description()); + if (property.defaultValue() != null) { + optionBuilder.set("defaultValue", property.defaultValue()); + } + if (property.experimental) { + optionBuilder.set("experimental", true); + } + if (!property.optional) { + optionBuilder.set("required", true); + } + if (!property.kind().equals("VALUE")) { + optionBuilder.set("kind", property.kind()); + } + if (property.provider) { + optionBuilder.set("provider", true); + optionBuilder.set("providerType", property.providerType.fqName()); + } + if (property.deprecated()) { + optionBuilder.set("deprecated", true); + } + if (property.merge()) { + optionBuilder.set("merge", true); + } + String method = property.builderMethod(); + if (method != null) { + optionBuilder.set("method", method); + } + if (property.configuredType != null) { + String finalPrefix; + if (property.kind().equals("LIST")) { + finalPrefix = prefix(prefix(prefix, property.key()), "*"); + } else { + finalPrefix = prefix(prefix, property.key()); + } + property.configuredType.properties() + .forEach(it -> writeProperty(optionsBuilder, finalPrefix, it)); + } + if (!property.allowedValues.isEmpty()) { + List allowedValues = new ArrayList<>(); + + for (ConfiguredOptionData.AllowedValue allowedValue : property.allowedValues) { + var allowedJson = Hson.Struct.builder() + .set("value", allowedValue.value()); + if (!allowedValue.description().isBlank()) { + allowedJson.set("description", allowedValue.description().trim()); + } + allowedValues.add(allowedJson.build()); + } + + optionBuilder.setStructs("allowedValues", allowedValues); + } + + optionsBuilder.add(optionBuilder.build()); + } + + private String prefix(String currentPrefix, String newSuffix) { + if (currentPrefix.isEmpty()) { + return newSuffix; + } + return currentPrefix + "." + newSuffix; + } + + static final class ProducerMethod { + private final boolean isStatic; + private final TypeName owningClass; + private final String methodName; + private final List methodParams; + + ProducerMethod(boolean isStatic, TypeName owningClass, String methodName, List methodParams) { + this.isStatic = isStatic; + this.owningClass = owningClass; + this.methodName = methodName; + this.methodParams = methodParams; + } + + @Override + public String toString() { + return owningClass.fqName() + + "#" + + methodName + "(" + + paramsToString(methodParams) + ")"; + } + } + + static final class ConfiguredProperty { + private final String builderMethod; + private final String key; + private final String description; + private final String defaultValue; + private final String type; + private final boolean experimental; + private final boolean optional; + private final String kind; + private final boolean provider; + private final TypeName providerType; + private final boolean deprecated; + private final boolean merge; + private final List allowedValues; + // if this is a nested type + private ConfiguredType configuredType; + + ConfiguredProperty(String builderMethod, + String key, + String description, + String defaultValue, + TypeName type, + boolean experimental, + boolean optional, + String kind, + boolean provider, + TypeName providerType, + boolean deprecated, + boolean merge, + List allowedValues) { + this.builderMethod = builderMethod; + this.key = key; + this.description = description; + this.defaultValue = defaultValue; + this.type = type.fqName(); + this.experimental = experimental; + this.optional = optional; + this.kind = kind; + this.provider = provider; + this.providerType = providerType == null ? type : providerType; + this.deprecated = deprecated; + this.merge = merge; + this.allowedValues = allowedValues; + } + + String builderMethod() { + return builderMethod; + } + + String key() { + return key; + } + + String description() { + return description; + } + + String defaultValue() { + return defaultValue; + } + + String type() { + return type; + } + + boolean experimental() { + return experimental; + } + + boolean optional() { + return optional; + } + + String kind() { + return kind; + } + + boolean deprecated() { + return deprecated; + } + + boolean merge() { + return merge; + } + + void nestedType(ConfiguredType nested) { + this.configuredType = nested; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfiguredProperty that = (ConfiguredProperty) o; + return key.equals(that.key); + } + + @Override + public int hashCode() { + return Objects.hash(key); + } + + @Override + public String toString() { + return key; + } + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/Javadoc.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/Javadoc.java new file mode 100644 index 00000000000..027594801a8 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/Javadoc.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.regex.Pattern; + +import static io.helidon.codegen.CodegenUtil.capitalize; + +/* +Possible improvements: +- @link - create a proper javadoc reference (i.e. always fully qualified reference), such as: + {@link:io.helidon.common.Type#method(java.lang.String)}, so we can generate a nice reference for docs +- @value - if possible, find the actual value (string, int etc.) and add it as `thevalue` +- @see - create a proper javadoc reference (as for @link) + */ +final class Javadoc { + private static final Pattern JAVADOC_CODE = Pattern.compile("\\{@code (.*?)}"); + private static final Pattern JAVADOC_LINK = Pattern.compile("\\{@link (.*?)}"); + private static final Pattern JAVADOC_LINKPLAIN = Pattern.compile("\\{@linkplain (.*?)}"); + private static final Pattern JAVADOC_VALUE = Pattern.compile("\\{@value (.*?)}"); + private static final Pattern JAVADOC_SEE = Pattern.compile("@see (.*?\n)"); + + private Javadoc() { + } + + /** + * Parses a Javadoc comment (provided as a string) into text that can be used for display/docs of the configuration option. + *

    + * The following steps are done: + *

      + *
    • {@code @param} is stripped from the text
    • + *
    • Any {@code @code} section: the code tag is removed, and surrounded with {@code '}
    • + *
    • Any {@code @link} section: the link tag is removed
    • + *
    • Any {@code @linkplain} section: the linkplain tag is removed
    • + *
    • Any {@code @value} section: the value tag is removed, {code #} is replaced with {@code .}
    • + *
    • Any {@code @see} section: the see tag is removed, prefixed with {@code See}, + * {code #} is replaced with {@code .}
    • + *
    • {@code @return} is stripped from the text, and the first letter is capitalized
    • + *
    + * + * @param docComment "raw" javadoc from the source code + * @return description of the option + */ + static String parse(String docComment) { + if (docComment == null) { + return ""; + } + + String javadoc = docComment; + int index = javadoc.indexOf("@param"); + if (index > -1) { + javadoc = docComment.substring(0, index); + } + // replace all {@code xxx} with 'xxx' + javadoc = JAVADOC_CODE.matcher(javadoc).replaceAll(it -> javadocCode(it.group(1))); + // replace all {@link ...} with just the link + javadoc = JAVADOC_LINK.matcher(javadoc).replaceAll(it -> javadocLink(it.group(1))); + // replace all {@link ...} with just the name + javadoc = JAVADOC_LINKPLAIN.matcher(javadoc).replaceAll(it -> javadocLink(it.group(1))); + // replace all {@value ...} with just the reference + javadoc = JAVADOC_VALUE.matcher(javadoc).replaceAll(it -> javadocValue(it.group(1))); + // replace all {@see ...} with just the reference + javadoc = JAVADOC_SEE.matcher(javadoc).replaceAll(it -> javadocSee(it.group(1))); + + int count = 9; + index = javadoc.indexOf(" @return"); + if (index == -1) { + count = 8; + index = javadoc.indexOf("@return"); + } + if (index > -1) { + javadoc = javadoc.substring(0, index) + capitalize(javadoc.substring(index + count).trim()); + } + + return javadoc.trim(); + } + + private static String javadocSee(String originalValue) { + return "See " + javadocValue(originalValue); + } + + private static String javadocCode(String originalValue) { + return '`' + originalValue + '`'; + } + + private static String javadocLink(String originalValue) { + return javadocValue(originalValue); + } + + private static String javadocValue(String originalValue) { + if (originalValue.startsWith("#")) { + return originalValue.substring(1); + } + return originalValue.replace('#', '.'); + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/OptionType.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/OptionType.java new file mode 100644 index 00000000000..c3b94483925 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/OptionType.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import io.helidon.common.types.TypeName; + +record OptionType(TypeName elementType, String kind) { +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandler.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandler.java new file mode 100644 index 00000000000..c8b7cadaf98 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandler.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +interface TypeHandler { + /** + * Discover all options of the configured type. + */ + TypeHandlerResult handle(); +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBase.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBase.java new file mode 100644 index 00000000000..236f9b98960 --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBase.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import io.helidon.codegen.CodegenContext; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.common.types.ElementKind.ENUM; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.COMMON_CONFIG; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.CONFIG; + +abstract class TypeHandlerBase { + static final String UNCONFIGURED_OPTION = "io.helidon.config.metadata.ConfiguredOption.UNCONFIGURED"; + + private final CodegenContext ctx; + + TypeHandlerBase(CodegenContext ctx) { + this.ctx = ctx; + } + + static Predicate isMine(TypeName type) { + TypeName withoutGenerics = type.genericTypeName(); + return info -> info.enclosingType().map(TypeName::genericTypeName).map(withoutGenerics::equals).orElse(true); + } + + // exactly one parameter - either common config, or Helidon config + static boolean hasConfigParam(TypedElementInfo info) { + List arguments = info.parameterArguments(); + if (arguments.size() != 1) { + return false; + } + TypeName argumentType = arguments.get(0).typeName(); + return CONFIG.equals(argumentType) || COMMON_CONFIG.equals(argumentType); + } + + /* + Method name is camel case (such as maxInitialLineLength) + result is dash separated and lower cased (such as max-initial-line-length). + Note that this same method was created in ConfigUtils in common-config, but since this + module should not have any dependencies in it a copy was left here as well. + */ + static String toConfigKey(String methodName) { + StringBuilder result = new StringBuilder(); + + char[] chars = methodName.toCharArray(); + for (char aChar : chars) { + if (Character.isUpperCase(aChar)) { + if (result.isEmpty()) { + result.append(Character.toLowerCase(aChar)); + } else { + result.append('-') + .append(Character.toLowerCase(aChar)); + } + } else { + result.append(aChar); + } + } + + return result.toString(); + } + + static String javadoc(String docComment) { + return Javadoc.parse(docComment); + } + + String key(TypedElementInfo elementInfo, ConfiguredOptionData configuredOption) { + String name = configuredOption.name(); + if (name == null || name.isBlank()) { + return toConfigKey(elementInfo.elementName()); + } + return name; + } + + String description(TypedElementInfo elementInfo, ConfiguredOptionData configuredOption) { + String desc = configuredOption.description(); + if (desc == null) { + return javadoc(elementInfo.description().orElse(null)); + } + return desc; + } + + String defaultValue(String defaultValue) { + return UNCONFIGURED_OPTION.equals(defaultValue) ? null : defaultValue; + } + + List allowedValues(ConfiguredOptionData configuredOption, TypeName type) { + if (type.equals(configuredOption.type()) || !configuredOption.allowedValues().isEmpty()) { + // this was already processed due to an explicit type defined in the annotation + // or allowed values explicitly configured in annotation + return configuredOption.allowedValues(); + } + return allowedValues(type); + } + + CodegenContext ctx() { + return ctx; + } + + List params(TypedElementInfo info) { + return info.parameterArguments() + .stream() + .map(TypedElementInfo::typeName) + .toList(); + } + + void addInterfaces(ConfiguredType type, TypeInfo typeInfo, TypeName requiredAnnotation) { + for (TypeInfo interfaceInfo : typeInfo.interfaceTypeInfo()) { + if (interfaceInfo.hasAnnotation(requiredAnnotation)) { + type.addInherited(interfaceInfo.typeName()); + } else { + addSuperClasses(type, interfaceInfo, requiredAnnotation); + } + } + } + + void addSuperClasses(ConfiguredType type, TypeInfo typeInfo, TypeName requiredAnnotation) { + Optional foundSuperType = typeInfo.superTypeInfo(); + if (foundSuperType.isEmpty()) { + return; + } + TypeInfo superClass = foundSuperType.get(); + + while (true) { + if (superClass.hasAnnotation(requiredAnnotation)) { + // we only care about the first one. This one should reference its superclass/interfaces + // if they are configured as well + type.addInherited(superClass.typeName()); + return; + } + + foundSuperType = superClass.superTypeInfo(); + if (foundSuperType.isEmpty()) { + return; + } + superClass = foundSuperType.get(); + } + } + + List allowedValuesEnum(ConfiguredOptionData data, TypeInfo enumInfo) { + if (!data.allowedValues().isEmpty()) { + // this was already processed due to an explicit type defined in the annotation + // or allowed values explicitly configured in annotation + return data.allowedValues(); + } + return allowedValuesEnum(enumInfo); + } + + private List allowedValuesEnum(TypeInfo enumInfo) { + List values = new ArrayList<>(); + ConfiguredOptionData.enumAllowedValues(values, enumInfo); + return values; + } + + private List allowedValues(TypeName type) { + return ctx().typeInfo(type) + .filter(it -> it.kind() == ENUM) + .map(this::allowedValuesEnum) + .orElseGet(List::of); + + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java new file mode 100644 index 00000000000..afc3b65a1fa --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.common.types.ElementKind.ENUM; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.BLUEPRINT; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.COMMON_CONFIG; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.CONFIGURED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.OPTION_CONFIGURED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.PROTOTYPE_FACTORY; + +/* + * Takes care of blueprints annotated with builder API only. + */ +class TypeHandlerBuilderApi extends TypeHandlerBase implements TypeHandler { + private final TypeInfo blueprint; + private final TypeName blueprintType; + + TypeHandlerBuilderApi(CodegenContext ctx, TypeInfo blueprint) { + super(ctx); + + this.blueprint = blueprint; + this.blueprintType = blueprint.typeName(); + } + + static TypeHandler create(CodegenContext ctx, TypeInfo typeInfo) { + return new TypeHandlerBuilderApi(ctx, typeInfo); + } + + /* + This is always: + - an interface + - uses annotations from builder-api + - return type is the one to use + */ + @Override + public TypeHandlerResult handle() { + TypeName prototype = prototype(blueprintType); + TypeName builderType = TypeName.builder(prototype) + .className("Builder") + .addEnclosingName(prototype.className()) + .build(); + TypeName targetType = targetType(blueprint, prototype); + String module = blueprint.module().orElse("unknown"); + + ConfiguredAnnotation configured = ConfiguredAnnotation.createBuilder(blueprint); + + ConfiguredType type = new ConfiguredType(configured, + prototype, + targetType, + true); + + addInterfaces(type, blueprint, CONFIGURED); + + type.addProducer(new ConfiguredType.ProducerMethod(true, prototype, "create", List.of(COMMON_CONFIG))); + type.addProducer(new ConfiguredType.ProducerMethod(true, prototype, "builder", List.of())); + + if (!targetType.equals(prototype)) { + // target type may not have the method (if it is for example PrivateKey) + type.addProducer(new ConfiguredType.ProducerMethod(false, targetType, "create", List.of(prototype))); + } + + // and now process all blueprint methods - must be non default, non static + // methods on this interface + + blueprint.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(TypeHandlerBase.isMine(blueprint.typeName())) + .filter(Predicate.not(ElementInfoPredicates::isStatic)) + .filter(Predicate.not(ElementInfoPredicates::isDefault)) + .filter(ElementInfoPredicates.hasAnnotation(OPTION_CONFIGURED)) + .forEach(it -> processBlueprintMethod(builderType, + type, + it)); + + return new TypeHandlerResult(targetType, + module, + type); + } + + @Override + void addInterfaces(ConfiguredType type, TypeInfo typeInfo, TypeName requiredAnnotation) { + for (TypeInfo interfaceInfo : typeInfo.interfaceTypeInfo()) { + if (interfaceInfo.hasAnnotation(requiredAnnotation)) { + TypeName ifaceTypeName = interfaceInfo.typeName(); + + if (interfaceInfo.hasAnnotation(BLUEPRINT)) { + String className = ifaceTypeName.className(); + if (className.endsWith("Blueprint")) { + className = className.substring(0, className.length() - "Blueprint".length()); + } + ifaceTypeName = TypeName.builder(ifaceTypeName) + .className(className) + .build(); + } + + type.addInherited(ifaceTypeName); + } else { + addSuperClasses(type, interfaceInfo, requiredAnnotation); + } + } + } + + private static TypeName prototype(TypeName blueprintType) { + String className = blueprintType.className(); + if (className.endsWith("Blueprint")) { + className = className.substring(0, className.length() - "Blueprint".length()); + } + return TypeName.builder(blueprintType) + .className(className) + .build() + .genericTypeName(); + } + + // if the type implements `Factory`, we want to return X, otherwise "pure" config object + private static TypeName targetType(TypeInfo blueprint, TypeName prototype) { + return blueprint.interfaceTypeInfo() + .stream() + .map(TypeInfo::typeName) + .filter(it -> PROTOTYPE_FACTORY.equals(it.genericTypeName())) + .filter(it -> it.typeArguments().size() == 1) + .map(it -> it.typeArguments().get(0)) + .findAny() + .orElse(prototype); + } + + private OptionType typeForBlueprintFromSignature(TypedElementInfo element, + ConfiguredOptionData annotation) { + // guess from method + + if (!ElementInfoPredicates.hasNoArgs(element)) { + throw new CodegenException("Method " + element + " is annotated with @Configured, " + + "yet it has a parameter. Interface methods must not have parameters.", + element.originatingElementValue()); + } + + TypeName returnType = element.typeName(); + if (ElementInfoPredicates.isVoid(element)) { + throw new CodegenException("Method " + element + " is annotated with @Configured, " + + "yet it is void. Interface methods must return the property type.", + element.originatingElementValue()); + } + + if (returnType.isOptional()) { + // may be an optional of list etc. + if (!(returnType.isMap() || returnType.isSet() || returnType.isList())) { + return new OptionType(returnType.typeArguments().get(0), "VALUE"); + } + returnType = returnType.typeArguments().get(0); + } + + if (returnType.isList() || returnType.isSet()) { + return new OptionType(returnType.typeArguments().get(0), "LIST"); + } + + if (returnType.isMap()) { + return new OptionType(returnType.typeArguments().get(1), "MAP"); + } + + return new OptionType(returnType.boxed(), annotation.kind()); + } + + private void processBlueprintMethod(TypeName typeName, ConfiguredType configuredType, TypedElementInfo elementInfo) { + // we always have exactly one option per method + ConfiguredOptionData data = ConfiguredOptionData.createBuilder(elementInfo); + + String name = key(elementInfo, data); + String description = description(elementInfo, data); + String defaultValue = defaultValue(data.defaultValue()); + boolean experimental = data.experimental(); + OptionType type = typeForBlueprintFromSignature(elementInfo, data); + boolean optional = defaultValue != null || data.optional(); + boolean deprecated = data.deprecated(); + + Optional enumType = ctx().typeInfo(type.elementType()) + .filter(it -> it.kind() == ENUM); + + List allowedValues; + + if (enumType.isPresent() && defaultValue != null) { + // prefix the default value with the enum name to make it more readable + defaultValue = type.elementType().className() + "." + defaultValue; + allowedValues = allowedValuesEnum(data, enumType.get()); + } else { + allowedValues = allowedValues(data, type.elementType()); + } + + List paramTypes = List.of(elementInfo.typeName()); + + ConfiguredType.ProducerMethod builderMethod = new ConfiguredType.ProducerMethod(false, + typeName, + elementInfo.elementName(), + paramTypes); + + ConfiguredType.ConfiguredProperty property = new ConfiguredType.ConfiguredProperty(builderMethod.toString(), + name, + description, + defaultValue, + type.elementType(), + experimental, + optional, + type.kind(), + data.provider(), + data.providerType(), + deprecated, + data.merge(), + allowedValues); + configuredType.addProperty(property); + } + +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java new file mode 100644 index 00000000000..70b1ef5ce7c --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.config.metadata.codegen.ConfiguredType.ProducerMethod; + +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.COMMON_CONFIG; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.CONFIG; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.META_CONFIGURED; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.META_OPTION; +import static io.helidon.config.metadata.codegen.ConfigMetadataTypes.META_OPTIONS; + +/* + * Takes care of blueprints annotated with builder API only. + */ +class TypeHandlerMetaApi extends TypeHandlerBase implements TypeHandler { + private final TypeInfo typeInfo; + private final TypeName typeName; + + TypeHandlerMetaApi(CodegenContext ctx, TypeInfo typeInfo) { + super(ctx); + + this.typeInfo = typeInfo; + this.typeName = typeInfo.typeName(); + } + + public static TypeHandler create(CodegenContext ctx, TypeInfo typeInfo) { + return new TypeHandlerMetaApi(ctx, typeInfo); + } + + @Override + public TypeHandlerResult handle() { + TypeInfo targetType; + boolean isBuilder; + String module; + Optional foundTarget = findBuilderTarget(new HashSet<>(), typeInfo); + ConfiguredAnnotation configured = ConfiguredAnnotation.createMeta(typeInfo.annotation(META_CONFIGURED)); + if (!configured.ignoreBuildMethod() + && foundTarget.isPresent()) { + // we want this both for abstract types and implementations + // this is a builder, we need the target type Builder + TypeName targetTypeName = foundTarget.get(); + targetType = ctx().typeInfo(targetTypeName, ElementInfoPredicates::isMethod) + .orElseThrow(() -> new IllegalStateException("Cannot find target type info for type " + + targetTypeName.fqName() + + ", discovered for type: " + typeInfo.typeName() + .fqName())); + isBuilder = true; + module = targetType.module().orElse("unknown"); + } else { + targetType = typeInfo; + isBuilder = false; + module = typeInfo.module().orElse("unknown"); + } + + /* + now we know whether this is + - a builder + known target class (result of builder() method) + - a standalone class (probably with public static create(Config) method) + - an interface/abstract class only used for inheritance + */ + ConfiguredType type = new ConfiguredType(configured, + typeName, + targetType.typeName(), + false); + + /* + we also need to know all superclasses / interfaces that are configurable so we can reference them + these may be from other modules, so we cannot create a single set of values from all types + */ + addSuperClasses(type, typeInfo, META_CONFIGURED); + addInterfaces(type, typeInfo, META_CONFIGURED); + + if (isBuilder) { + // builder + processBuilderType(typeInfo, type, typeName, targetType); + } else { + // standalone class with create method(s), or interface/abstract class + processTargetType(typeInfo, type, typeName, type.standalone()); + } + + return new TypeHandlerResult(targetType.typeName(), module, type); + } + + List findConfiguredOptionAnnotations(TypedElementInfo elementInfo) { + if (elementInfo.hasAnnotation(META_OPTIONS)) { + Annotation metaOptions = elementInfo.annotation(META_OPTIONS); + return metaOptions.annotationValues() + .stream() + .flatMap(List::stream) + .map(it -> ConfiguredOptionData.createMeta(ctx(), it)) + .toList(); + } + + if (elementInfo.hasAnnotation(META_OPTION)) { + Annotation metaOption = elementInfo.annotation(META_OPTION); + return List.of(ConfiguredOptionData.createMeta(ctx(), metaOption)); + } + + return List.of(); + } + + void processBuilderMethod(TypeName typeName, + ConfiguredType configuredType, + TypedElementInfo elementInfo, + BiFunction optionTypeMethod, + BiFunction> builderParamsMethod) { + List options = findConfiguredOptionAnnotations(elementInfo); + if (options.isEmpty()) { + return; + } + + for (ConfiguredOptionData data : options) { + if (!data.configured()) { + continue; + } + String name = key(elementInfo, data); + String description = description(elementInfo, data); + String defaultValue = defaultValue(data.defaultValue()); + boolean experimental = data.experimental(); + OptionType type = optionTypeMethod.apply(elementInfo, data); + boolean optional = defaultValue != null || data.optional(); + boolean deprecated = data.deprecated(); + List allowedValues = allowedValues(data, type.elementType()); + + List paramTypes = builderParamsMethod.apply(elementInfo, type); + + ProducerMethod builderMethod = new ProducerMethod(false, + typeName, + elementInfo.elementName(), + paramTypes); + + ConfiguredType.ConfiguredProperty property = new ConfiguredType.ConfiguredProperty(builderMethod.toString(), + name, + description, + defaultValue, + type.elementType(), + experimental, + optional, + type.kind(), + data.provider(), + data.providerType(), + deprecated, + data.merge(), + allowedValues); + configuredType.addProperty(property); + } + } + + // annotated type or type methods (not a builder) + private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName typeName, boolean standalone) { + // go through all methods, find all create methods and create appropriate configured producers for them + // if there is a builder, add the builder producer as well + + List methods = typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + // public, package local or protected + .filter(Predicate.not(ElementInfoPredicates::isPrivate)) + // static + .filter(ElementInfoPredicates::isStatic) + .toList(); + + // either this is a target class (such as an interface with create method) + // or this is an interface/abstract class inherited by builders + boolean isTargetType = false; + List validMethods = new LinkedList<>(); + TypedElementInfo configCreator = null; + + // now we have just public static methods, let's look for create/builder + for (TypedElementInfo method : methods) { + String name = method.elementName(); + + if ("create".equals(name)) { + if (method.typeName().genericTypeName().equals(typeName.genericTypeName())) { + validMethods.add(method); + List parameters = method.parameterArguments(); + if (parameters.size() == 1) { + TypeName paramType = parameters.get(0).typeName(); + if (paramType.equals(CONFIG) || paramType.equals(COMMON_CONFIG)) { + configCreator = method; + } + } + isTargetType = true; + } + } else if (name.equals("builder")) { + throw new CodegenException("Type " + typeName.fqName() + " is marked with @Configured" + + ", yet it has a static builder() method. Please mark the builder instead " + + "of this class.", + typeInfo.originatingElementValue()); + } + } + + if (isTargetType) { + if (configCreator != null) { + type.addProducer(new ProducerMethod(true, + typeName, + configCreator.elementName(), + params(configCreator))); + } + + // now let's find all methods with @ConfiguredOption + for (TypedElementInfo validMethod : validMethods) { + List options = findConfiguredOptionAnnotations(validMethod); + + if (options.isEmpty()) { + continue; + } + + for (ConfiguredOptionData data : options) { + if ((data.name() == null || data.name().isBlank()) && !data.merge()) { + throw new CodegenException("ConfiguredOption on " + typeName.fqName() + "." + + validMethod + + " does not have value defined. It is mandatory on non-builder " + + "methods", + typeInfo.originatingElementValue()); + } + + if (data.description() == null || data.description().isBlank()) { + throw new CodegenException("ConfiguredOption on " + typeName.fqName() + "." + validMethod + + " does not have description defined. It is mandatory on non-builder " + + "methods", + typeInfo.originatingElementValue()); + } + + if (data.type() == null) { + // this is the default value + data.type(TypeNames.STRING); + } + + ConfiguredType.ConfiguredProperty prop = new ConfiguredType.ConfiguredProperty(null, + data.name(), + data.description(), + data.defaultValue(), + data.type(), + data.experimental(), + data.optional(), + data.kind(), + data.provider(), + data.providerType(), + data.deprecated(), + data.merge(), + data.allowedValues()); + type.addProperty(prop); + } + } + } else { + // this must be a class/interface used by other classes to extend, so we care about all builder style + // methods + if (standalone) { + throw new CodegenException("Type " + typeName.fqName() + " is marked as standalone configuration unit, " + + "yet it does have " + + "neither a builder method, nor a create method", + typeInfo.originatingElementValue()); + } + + typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) // methods + .filter(Predicate.not(ElementInfoPredicates::isPrivate)) // public, package or protected + .filter(Predicate.not(ElementInfoPredicates::isStatic)) // not static + .filter(TypeHandlerMetaApi.isMine(typeName)) // declared on this type + .forEach(it -> processBuilderMethod(typeName, type, it)); + } + } + + // annotated builder methods + private void processBuilderType(TypeInfo typeInfo, ConfiguredType type, TypeName typeName, TypeInfo targetType) { + type.addProducer(new ProducerMethod(false, typeName, "build", List.of())); + + TypeName targetTypeName = targetType.typeName(); + // check if static TargetType create(Config) exists + if (targetType.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(ElementInfoPredicates::isStatic) + .filter(Predicate.not(ElementInfoPredicates::isPrivate)) + .filter(ElementInfoPredicates.elementName("create")) + .filter(TypeHandlerMetaApi::hasConfigParam) + .anyMatch(TypeHandlerMetaApi.isMine(targetTypeName))) { + + type.addProducer(new ProducerMethod(true, + targetTypeName, + "create", + List.of(COMMON_CONFIG))); + } + + // find all public methods annotated with @ConfiguredOption + typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) // methods + .filter(Predicate.not(ElementInfoPredicates::isPrivate)) // not private + .filter(TypeHandlerMetaApi.isMine(typeName)) // declared on this type + .filter(it -> it.hasAnnotation(META_OPTION) || it.hasAnnotation(META_OPTIONS)) + .forEach(it -> processBuilderMethod(typeName, type, it)); + } + + private List builderMethodParams(TypedElementInfo elementInfo, OptionType type) { + return params(elementInfo); + } + + private void processBuilderMethod(TypeName typeName, ConfiguredType configuredType, TypedElementInfo elementInfo) { + processBuilderMethod(typeName, configuredType, elementInfo, this::optionType, this::builderMethodParams); + } + + private OptionType optionType(TypedElementInfo elementInfo, ConfiguredOptionData annotation) { + if (annotation.type() == null || annotation.type().equals(META_OPTION)) { + // guess from method + + List parameters = elementInfo.parameterArguments(); + if (parameters.size() != 1) { + throw new CodegenException("Method " + elementInfo.elementName() + + " is annotated with @ConfiguredOption, " + + "yet it does not have explicit type, or exactly one parameter", + typeInfo.originatingElementValue()); + } else { + TypedElementInfo parameter = parameters.iterator().next(); + TypeName paramType = parameter.typeName(); + + if (paramType.isList() || paramType.isSet()) { + return new OptionType(paramType.typeArguments().get(0), "LIST"); + } + + if (paramType.isMap()) { + return new OptionType(paramType.typeArguments().get(1), "MAP"); + } + + return new OptionType(paramType.boxed(), annotation.kind()); + } + + } else { + // use the one defined on annotation + return new OptionType(annotation.type(), annotation.kind()); + } + } + + private Optional findBuilderTarget(Set processed, TypeInfo typeInfo) { + // non-private build method exists that has no parameters, not static, and returns a type + return typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(Predicate.not(ElementInfoPredicates::isStatic)) + .filter(ElementInfoPredicates::hasNoArgs) + .filter(ElementInfoPredicates.elementName("build")) + .filter(Predicate.not(ElementInfoPredicates::isVoid)) + .filter(Predicate.not(ElementInfoPredicates::isPrivate)) + .findFirst() + .map(it -> it.typeName()); + } +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerResult.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerResult.java new file mode 100644 index 00000000000..555ae30122b --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerResult.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.codegen; + +import io.helidon.common.types.TypeName; + +/** + * Result of annotation processing. + * + * @param targetType type that is configured (result of the builder, runtime type of a prototype) + * @param moduleName module of the type + * @param configuredType collected configuration metadata + */ +record TypeHandlerResult(TypeName targetType, + String moduleName, + ConfiguredType configuredType) { +} diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/package-info.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/package-info.java new file mode 100644 index 00000000000..9883931f32b --- /dev/null +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Codegen for Helidon Config Metadata. + *

    + * The code generator triggers on both Helidon Builder configuration annotations, and on + * Helidon Config Metadata configuration annotations. + *

    + * This codegen generates a {@code META-INF/helidon/config-metadata.json} for configuration options in the module. + */ +package io.helidon.config.metadata.codegen; diff --git a/config/metadata/codegen/src/main/java/module-info.java b/config/metadata/codegen/src/main/java/module-info.java new file mode 100644 index 00000000000..560665638ae --- /dev/null +++ b/config/metadata/codegen/src/main/java/module-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Codegen for Helidon Config Metadata. + * + * @see io.helidon.config.metadata.codegen + */ +module io.helidon.config.metadata.codegen { + requires io.helidon.codegen; + requires io.helidon.metadata.hson; + + exports io.helidon.config.metadata.codegen; + + provides io.helidon.codegen.spi.CodegenExtensionProvider + with io.helidon.config.metadata.codegen.ConfigMetadataCodegenProvider; +} \ No newline at end of file diff --git a/config/metadata/docs/etc/spotbugs/exclude.xml b/config/metadata/docs/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..942fcbc4aa4 --- /dev/null +++ b/config/metadata/docs/etc/spotbugs/exclude.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/metadata/docs/pom.xml b/config/metadata/docs/pom.xml new file mode 100644 index 00000000000..d13301dc882 --- /dev/null +++ b/config/metadata/docs/pom.xml @@ -0,0 +1,130 @@ + + + + + 4.0.0 + + io.helidon.config + helidon-config-metadata-project + 4.2.0-SNAPSHOT + + + io.helidon.config.metadata + helidon-config-metadata-docs + Helidon Config Metadata Docs + + Generator of Helidon documentation for config metadata (uses the `config-metadata.json` generated by the + `helidon-config-metadata-codegen` module + + + + io.helidon.config.metadata.docs.Main + + true + true + true + true + etc/spotbugs/exclude.xml + + + + + jakarta.json.bind + jakarta.json.bind-api + + + io.helidon.logging + helidon-logging-common + + + com.github.jknack + handlebars + + + org.eclipse + yasson + + + org.slf4j + slf4j-jdk14 + runtime + + + + org.mockito + mockito-core + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + io.helidon + helidon-all + pom + runtime + + + io.helidon.logging + helidon-logging-slf4j + + + io.helidon.logging + helidon-logging-log4j + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + -proc:none + + + + org.codehaus.mojo + exec-maven-plugin + + java + true + + -classpath + + ${mainClass} + + + + + + diff --git a/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmAllowedValue.java b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmAllowedValue.java new file mode 100644 index 00000000000..288f58ff80e --- /dev/null +++ b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmAllowedValue.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.docs; + +/** + * JSON-B type for Allowed values in config-metadata.json. + */ +public class CmAllowedValue { + private String value; + private String description; + + /** + * Required constructor. + */ + public CmAllowedValue() { + } + + /** + * Required getter. + * + * @return allowed value + */ + public String getValue() { + return value; + } + + /** + * Required setter. + * + * @param value allowed value + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Required getter. + * + * @return description of the allowed value + */ + public String getDescription() { + return description; + } + + /** + * Required setter. + * + * @param description allowed value description + */ + public void setDescription(String description) { + this.description = description; + } +} diff --git a/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmModule.java b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmModule.java new file mode 100644 index 00000000000..94d9520222f --- /dev/null +++ b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmModule.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.docs; + +import java.util.List; + +/** + * JSON-B type for module in config-metadata.json. + */ +public class CmModule { + private String module; + private List types; + + /** + * Required constructor. + */ + public CmModule() { + } + + /** + * Required getter. + * + * @return module name + */ + public String getModule() { + return module; + } + + /** + * Required setter. + * + * @param module module name + */ + public void setModule(String module) { + this.module = module; + } + + /** + * Required getter. + * + * @return configured types in this module + */ + public List getTypes() { + return types; + } + + /** + * Required setter. + * + * @param types configured types in this module + */ + public void setTypes(List types) { + this.types = types; + } + + @Override + public String toString() { + return module; + } +} diff --git a/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmOption.java b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmOption.java new file mode 100644 index 00000000000..c8732aa0478 --- /dev/null +++ b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmOption.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.docs; + +import java.util.List; + +/** + * JSON-B type for configured option in config-metadata.json. + */ +public class CmOption { + private String key; + private String description; + private String method; + private String type = "string"; + private String defaultValue; + private boolean required = false; + private boolean experimental = false; + private boolean deprecated = false; + private boolean provider = false; + private String providerType; + private boolean merge = false; + private Kind kind = Kind.VALUE; + private String refType; + private List allowedValues; + + /** + * Required constructor. + */ + public CmOption() { + } + + /** + * Required getter. + * + * @return list of allowed values of this option + */ + public List getAllowedValues() { + return allowedValues; + } + + /** + * Required setter. + * + * @param allowedValues allowed values + */ + public void setAllowedValues(List allowedValues) { + this.allowedValues = allowedValues; + } + + /** + * Required getter. + * + * @return option kind + */ + public Kind getKind() { + return kind; + } + + /** + * Required setter. + * + * @param kind option kind + */ + public void setKind(Kind kind) { + this.kind = kind; + } + + /** + * Required getter. + * + * @return option config key + */ + public String getKey() { + return key; + } + + /** + * Required setter. + * + * @param key config key + */ + public void setKey(String key) { + this.key = key; + } + + /** + * Required getter. + * + * @return option description + */ + public String getDescription() { + return description; + } + + /** + * Required setter. + * + * @param description option description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Required getter. + * + * @return option method + */ + public String getMethod() { + return method; + } + + /** + * Required setter. + * + * @param method option method + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Required getter. + * + * @return option type + */ + public String getType() { + return type; + } + + /** + * Required setter. + * + * @param type option type + */ + public void setType(String type) { + this.type = type; + } + + /** + * Required getter. + * + * @return option default value + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * Required setter. + * + * @param defaultValue option default value + */ + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Required getter. + * + * @return whether this option is required + */ + public boolean isRequired() { + return required; + } + + /** + * Required setter. + * + * @param required option required + */ + public void setRequired(boolean required) { + this.required = required; + } + + /** + * Required getter. + * + * @return whether this option is experimental + */ + public boolean isExperimental() { + return experimental; + } + + /** + * Required setter. + * + * @param experimental whether this option is experimental + */ + public void setExperimental(boolean experimental) { + this.experimental = experimental; + } + + /** + * Required getter. + * + * @return whether this option is deprecated + */ + public boolean isDeprecated() { + return deprecated; + } + + /** + * Required setter. + * + * @param deprecated whether this option is deprecated + */ + public void setDeprecated(boolean deprecated) { + this.deprecated = deprecated; + } + + /** + * Required getter. + * + * @return option refType + */ + public String getRefType() { + return refType; + } + + /** + * Required setter. + * + * @param refType option refType + */ + public void setRefType(String refType) { + this.refType = refType; + } + + /** + * Required getter. + * + * @return whether this option type is a service, and any service implementation may satisfy it + */ + public boolean isProvider() { + return provider; + } + + /** + * Required setter. + * + * @param provider whether this option is a provider + */ + public void setProvider(boolean provider) { + this.provider = provider; + } + + /** + * Required getter. + * + * @return option provider type (service interface) + */ + public String getProviderType() { + return providerType; + } + + /** + * Required setter. + * + * @param providerType option provider type (service interface) + */ + public void setProviderType(String providerType) { + this.providerType = providerType; + } + + /** + * Required getter. + * + * @return whether this option merges child type into this type without a config key + */ + public boolean isMerge() { + return merge; + } + + /** + * Required setter. + * + * @param merge whether to merge this option + */ + public void setMerge(boolean merge) { + this.merge = merge; + } + + @Override + public String toString() { + return key + " (" + type + ")" + (merge ? " merged" : ""); + } + + /** + * Option kind. + */ + public enum Kind { + /** + * Option is a single value (leaf node). + * Example: server port + */ + VALUE, + /** + * Option is a list of values (either primitive, String or object nodes). + * Example: cipher suite in SSL, server sockets + */ + LIST, + /** + * Option is a map of strings to primitive type or String. + * Example: tags in tracing, CDI configuration + */ + MAP + } +} diff --git a/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmReference.java b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmReference.java new file mode 100644 index 00000000000..e461a047cc5 --- /dev/null +++ b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmReference.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.docs; + +/** + * Type required for template processing for config reference. + */ +public class CmReference { + private final String file; + private final String title; + + CmReference(String file, String title) { + this.file = file; + this.title = title; + } + + /** + * File name that was generated. + * + * @return file name + */ + public String file() { + return file; + } + + /** + * Title of the file. + * + * @return title + */ + public String title() { + return title; + } +} diff --git a/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmType.java b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmType.java new file mode 100644 index 00000000000..783861c4725 --- /dev/null +++ b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/CmType.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.docs; + +import java.util.List; + +/** + * JSON-B type for configured type in config-metadata.json. + */ +public class CmType { + private String type; + private String title; + private String annotatedType; + private List options; + private String description; + private String prefix; + private boolean standalone; + private List inherits; + private List producers; + private List provides; + private String typeReference; + + /** + * Required constructor. + */ + public CmType() { + } + + /** + * Required getter. + * + * @return type description + */ + public String getDescription() { + return description; + } + + /** + * Required setter. + * + * @param description type description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Required getter. + * + * @return type config prefix + */ + public String getPrefix() { + return prefix; + } + + /** + * Required setter. + * + * @param prefix type config prefix + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + /** + * Required getter. + * + * @return whether this is a standalone configuration type + */ + public boolean isStandalone() { + return standalone; + } + + /** + * Required setter. + * + * @param standalone whether this is a standalone configuration type + */ + public void setStandalone(boolean standalone) { + this.standalone = standalone; + } + + /** + * Required getter. + * + * @return list of inherited types + */ + public List getInherits() { + return inherits; + } + + /** + * Required setter. + * + * @param inherits types to inherit + */ + public void setInherits(List inherits) { + this.inherits = inherits; + } + + /** + * Required getter. + * + * @return list of producer methods + */ + public List getProducers() { + return producers; + } + + /** + * Required setter. + * + * @param producers list of producer methods + */ + public void setProducers(List producers) { + this.producers = producers; + } + + /** + * Required getter. + * + * @return type name + */ + public String getType() { + return type; + } + + /** + * Required setter. + * + * @param type type name + */ + public void setType(String type) { + this.type = type; + } + + /** + * Required getter. + * + * @return type that is annotated + */ + public String getAnnotatedType() { + return annotatedType; + } + + /** + * Required setter. + * + * @param annotatedType the annotated type + */ + public void setAnnotatedType(String annotatedType) { + this.annotatedType = annotatedType; + } + + /** + * Required getter. + * + * @return type list of options of this type + */ + public List getOptions() { + return options; + } + + /** + * Required setter. + * + * @param options type options + */ + public void setOptions(List options) { + this.options = options; + } + + /** + * Required getter. + * + * @return type list of provided services + */ + public List getProvides() { + return provides; + } + + /** + * Required setter. + * + * @param provides provided services + */ + public void setProvides(List provides) { + this.provides = provides; + } + + /** + * Whether this type represents a service implementation. + * + * @return whether this type provides an implementation of a service + */ + public boolean hasProvides() { + return provides != null && !provides.isEmpty(); + } + + /** + * Required getter. + * + * @return type reference + */ + public String getTypeReference() { + return typeReference; + } + + /** + * Required setter. + * + * @param typeReference type reference + */ + public void setTypeReference(String typeReference) { + this.typeReference = typeReference; + } + + /** + * Required getter. + * + * @return type title + */ + public String getTitle() { + return title; + } + + /** + * Required setter. + * + * @param title type title + */ + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return getType(); + } +} diff --git a/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/ConfigDocs.java b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/ConfigDocs.java new file mode 100644 index 00000000000..8278667b8ac --- /dev/null +++ b/config/metadata/docs/src/main/java/io/helidon/config/metadata/docs/ConfigDocs.java @@ -0,0 +1,852 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.metadata.docs; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.lang.System.Logger.Level; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.io.URLTemplateSource; +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import org.eclipse.yasson.YassonConfig; + +/** + * Entry point to generate config documentation for Helidon Config reference. + *

    + * This module can either be used through {@link io.helidon.config.metadata.docs.Main}, or via this class. + * + * @see #create(java.nio.file.Path) + * @see #process() + */ +public class ConfigDocs { + private static final System.Logger LOGGER = System.getLogger(ConfigDocs.class.getName()); + private static final String CONFIG_REFERENCE_ADOC = "config_reference.adoc"; + private static final String METADATA_JSON_LOCATION = "META-INF/helidon/config-metadata.json"; + private static final String RELATIVE_PATH_ADOC = "{rootdir}/config/"; + private static final Pattern MODULE_PATTERN = Pattern.compile("(.*?)(\\.spi)?\\.([a-zA-Z0-9]*?)"); + private static final Pattern COPYRIGHT_LINE_PATTERN = Pattern.compile(".*Copyright \\(c\\) (.*) Oracle and/or its " + + "affiliates."); + private static final Jsonb JSON_B = JsonbBuilder.create(new YassonConfig().withFailOnUnknownProperties(true)); + private static final Map TYPE_MAPPING; + + static { + Map typeMapping = new HashMap<>(); + typeMapping.put("java.lang.String", "string"); + typeMapping.put("java.lang.Integer", "int"); + typeMapping.put("java.lang.Boolean", "boolean"); + typeMapping.put("java.lang.Long", "long"); + typeMapping.put("java.lang.Character", "char"); + typeMapping.put("java.lang.Float", "float"); + typeMapping.put("java.lang.Double", "double"); + TYPE_MAPPING = Map.copyOf(typeMapping); + } + + private final Path path; + + private ConfigDocs(Path path) { + this.path = path; + } + + /** + * Create a new instance that will update config reference documentation in the {code targetPath}. + * + * @param targetPath path of the config reference documentation, must contain the {@value #CONFIG_REFERENCE_ADOC} + * file, or be empty + * @return new instance of config documentation to call {@link #process()} on + */ + public static ConfigDocs create(Path targetPath) { + return new ConfigDocs(targetPath); + } + + static String titleFromFileName(String fileName) { + String title = fileName; + // string .adoc + if (title.endsWith(".adoc")) { + title = title.substring(0, title.length() - 5); + } + if (title.startsWith("io_helidon_")) { + title = title.substring("io_helidon_".length()); + int i = title.lastIndexOf('_'); + if (i != -1) { + String simpleName = title.substring(i + 1); + String thePackage = title.substring(0, i); + title = simpleName + " (" + thePackage.replace('_', '.') + ")"; + } + } + return title; + } + + // translate HTML to asciidoc + static String translateHtml(String text) { + String result = text; + //

    + result = result.replaceAll("\n\\s*

    ", "\n"); + result = result.replaceAll("\\s*

    ", "\n"); + result = result.replaceAll("

    ", ""); + //
    • + result = result.replaceAll("\\s*
    • \\s*", ""); + result = result.replaceAll("\\s*
    \\s*", "\n\n"); + result = result.replaceAll("\\s*\\s*", "\n\n"); + result = result.replaceAll("\n\\s*