diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 0000000000000..75640ba3e7d7c --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,93 @@ +# Documentation in https://dependabot.com/docs/config-file/ +version: 1 +update_configs: + - package_manager: "java:maven" + directory: "/" + update_schedule: "daily" + allowed_updates: + - match: + update_type: "security" + - match: + dependency_name: "org.apache.activemq:artemis-core-client" + - match: + dependency_name: "org.apache.activemq:artemis-jms-client" + - match: + dependency_name: "org.apache.activemq:artemis-server" + - match: + dependency_name: "org.apache.activemq:artemis-commons" + - match: + dependency_name: "org.flywaydb:flyway-core" + - match: + dependency_name: "org.freemarker:freemarker" + - match: + dependency_name: "org.eclipse.jgit:org.eclipse.jgit" + - match: + dependency_name: "io.fabric8:kubernetes-client-bom" + - match: + dependency_name: "org.apache.httpcomponents:httpclient" + - match: + dependency_name: "org.apache.httpcomponents:httpasyncclient" + - match: + dependency_name: "org.apache.httpcomponents:httpcore" + - match: + dependency_name: "org.quartz-scheduler:quartz" + - match: + dependency_name: "com.cronutils:cron-utils" + - match: + dependency_name: "org.eclipse:yasson" + # JDBC Drivers + - match: + dependency_name: "org.postgresql:postgresql" + - match: + dependency_name: "org.mariadb.jdbc:mariadb-java-client" + - match: + dependency_name: "mysql:mysql-connector-java" + - match: + dependency_name: "org.apache.derby:derbyclient" + - match: + dependency_name: "com.microsoft.sqlserver:mssql-jdbc" + # Kafka + - match: + dependency_name: "org.apache.kafka:kafka-clients" + - match: + dependency_name: "org.apache.kafka:kafka-streams" + - match: + dependency_name: "org.apache.kafka:kafka_2.12" + - match: + dependency_name: "org.apache.zookeeper:zookeeper" + # Debezium + - match: + dependency_name: "io.debezium:debezium-core" + # Scala + - match: + dependency_name: "org.scala-lang:scala-reflect" + - match: + dependency_name: "org.scala-lang:scala-library" + - match: + dependency_name: "org.scala-lang:scala-compiler" + - match: + dependency_name: "net.alchim31.maven:scala-maven-plugin" + # Smallrye + - match: + dependency_name: "io.smallrye:smallrye-jwt" + # Tika + - match: + dependency_name: "org.apache.tika:tika-parsers" + # RX Java 2 + - match: + dependency_name: "io.reactivex.rxjava2:rxjava" + # Test dependencies + - match: + dependency_name: "io.rest-assured:rest-assured" + - match: + dependency_name: "io.rest-assured:json-schema-validator" + - match: + dependency_name: "org.junit:junit-bom" + - match: + dependency_name: "org.assertj:assertj-core" + - match: + dependency_name: "org.testcontainers:testcontainers" + - match: + dependency_name: "org.testcontainers:junit-jupiter" + - match: + dependency_name: "org.mockito:mockito-core" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f59ffe5e0d68a..2dd5915408ce5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Report a bug in Quarkus title: '' -labels: bug +labels: kind/bug assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md index 501498789e741..88ba1f93b76bf 100644 --- a/.github/ISSUE_TEMPLATE/epic.md +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -2,7 +2,7 @@ name: Epic about: " A large piece of work requiring high-level visibility and planning" title: '' -labels: Epic +labels: kind/epic assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/extension_proposal.md b/.github/ISSUE_TEMPLATE/extension_proposal.md index 32b46ba0e0dcd..2c464154e2940 100644 --- a/.github/ISSUE_TEMPLATE/extension_proposal.md +++ b/.github/ISSUE_TEMPLATE/extension_proposal.md @@ -2,7 +2,7 @@ name: Extension Proposal about: Propose a new extension in Quarkus title: '' -labels: extension-proposal +labels: kind/extension-proposal assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 5264caf315ff8..4a64304b61371 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Request or propose a new feature title: '' -labels: enhancement +labels: kind/enhancement assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/housekeeping.md b/.github/ISSUE_TEMPLATE/housekeeping.md new file mode 100644 index 0000000000000..88487e4539ef6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/housekeeping.md @@ -0,0 +1,14 @@ +--- +name: Housekeeping +about: A generalized task or cleanup not associated with a bug report or enhancement +title: '' +labels: area/housekeeping +assignees: '' + +--- + +**Description** +(Describe the task here.) + +**Implementation ideas** +(If you have any implementation ideas, they can go here.) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 30646816dd2fe..4231f9dd88167 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -2,7 +2,7 @@ name: Question about: I have a question about something... title: '' -labels: question +labels: kind/question assignees: '' --- diff --git a/ADOPTERS.md b/ADOPTERS.md index 1e093f273a45f..a281e8315f20e 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -13,4 +13,5 @@ If any organization would like get added or removed please make a pull request b | Organization | Reference | |---------------|------------| |GoWithFlow | https://quarkus.io/blog/gowithflow-chooses-quarkus-to-deliver-fast-to-production/ | +|Talkdesk | https://quarkus.io/blog/talkdesk-chooses-quarkus-for-fast-innovation/ | |Vodafone Greece| https://quarkus.io/blog/vodafone-greece-replaces-spring-boot/| diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fcfa1d63b7a24..fe98c74a608d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ If you have not done so on this machine, you need to: * Install Git and configure your GitHub access * Install Java SDK (OpenJDK recommended) -* Install [GraalVM](http://www.graalvm.org/downloads/) (community edition is enough) +* Install [GraalVM](https://quarkus.io/guides/building-native-image) * Install platform C developer tools: * Linux * Make sure headers are available on your system (you'll hit 'Basic header file missing ()' error if they aren't). diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c11c706d73e67..2425fffbc6149 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,6 +3,7 @@ trigger: branches: include: - master + - '1.1' pr: branches: @@ -18,6 +19,7 @@ pr: - LICENSE.txt - dco.txt - .github/ISSUE_TEMPLATE/*.md + - .dependabot/config.yml variables: MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository/ diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index 0f3b2a1d90516..b661ca40d32ae 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -106,6 +106,11 @@ quarkus-agroal-deployment ${project.version} + + io.quarkus + quarkus-agroal-spi + ${project.version} + io.quarkus quarkus-artemis-core @@ -126,6 +131,11 @@ quarkus-artemis-jms-deployment ${project.version} + + io.quarkus + quarkus-config-yaml-deployment + ${project.version} + io.quarkus quarkus-hibernate-validator-deployment @@ -156,6 +166,11 @@ quarkus-kafka-client-deployment ${project.version} + + io.quarkus + quarkus-kafka-streams-deployment + ${project.version} + io.quarkus quarkus-smallrye-reactive-streams-operators-deployment @@ -186,6 +201,11 @@ quarkus-smallrye-metrics-deployment ${project.version} + + io.quarkus + quarkus-smallrye-metrics-spi + ${project.version} + io.quarkus quarkus-smallrye-openapi-common-deployment @@ -366,6 +386,11 @@ quarkus-swagger-ui-deployment ${project.version} + + io.quarkus + quarkus-elytron-security-common-deployment + ${project.version} + io.quarkus quarkus-elytron-security-deployment @@ -481,16 +506,41 @@ quarkus-spring-data-deployment ${project.version} + + io.quarkus + quarkus-spring-boot-properties-deployment + ${project.version} + io.quarkus quarkus-jgit-deployment ${project.version} + + io.quarkus + quarkus-jsch-deployment + ${project.version} + io.quarkus quarkus-vault-deployment ${project.version} + + io.quarkus + quarkus-logging-json-deployment + ${project.version} + + + io.quarkus + quarkus-logging-sentry-deployment + ${project.version} + + + io.quarkus + quarkus-logging-gelf-deployment + ${project.version} + io.quarkus @@ -503,6 +553,17 @@ quarkus-scala-deployment ${project.version} + + + io.quarkus + quarkus-qute-deployment + ${project.version} + + + io.quarkus + quarkus-resteasy-qute-deployment + ${project.version} + @@ -546,6 +607,11 @@ quarkus-elytron-security-jdbc-deployment ${project.version} + + io.quarkus + quarkus-elytron-security-ldap-deployment + ${project.version} + io.quarkus quarkus-security-test-utils diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 912ea68cfca06..7fc1f8a5b33f9 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -15,29 +15,29 @@ 1.11 - 2.1.1.Final - 4.4.1.Final + 2.1.2.Final + 4.4.2.Final 0.31.0 0.4.1 0.2.3 - 0.1.5 + 0.1.8 0.2.0 0.0.12 0.34.0 - 3.0.0.Final + 3.0.1.Final 1.0.0.Final 1.3 1.0.1 1.3.1 1.0 1.3.4 - 1.3.9 + 1.5.1 2.1.0 - 2.3.1 + 2.3.2 1.1.20 1.3.2 - 2.1.2 - 2.0.10 + 4.0.0 + 2.0.12 1.0.11 1.0.10 1.0.10 @@ -76,42 +76,40 @@ 19.2.1 1.0.0.Final - 2.9.10.20191020 + 2.10.2 1.0.0.Final 3.9 1.13 1.3.4 6.1.0.Final - 5.4.9.Final - 6.0.0.Beta2 + 5.4.10.Final + 6.0.0.Beta3 5.10.0.Final 1.1.1.Final 1.7 7.6.0.Final - 7.4.0 + 7.5.0 1.3.8 - 2.2.13 + 2.2.17 1.0.6.Final 2.1.0.Final - 1.7.25 + 1.7.29 1.2.0.Final 1.5.0.Final-format-001 1.0.1.Final 2.0.0.Alpha4 1.8.7.Final 3.0.0.Final - 3.8.3 - - 3.8.3-01 - 4.5.9 - 4.4.11 + 3.8.4 + 4.5.10 + 4.4.13 4.1.4 - 9.0.1 + 9.0.2 2.3.2 1.4.197 - 42.2.8 - 2.4.4 - 8.0.17 + 42.2.9 + 2.5.3 + 8.0.19 7.2.1.jre8 10.14.2.0 1.2.6 @@ -125,15 +123,16 @@ 2.6.2 4.1.42.Final 1.0.3 - 1.12.3 + 1.12.4 3.3.2.Final 0.0.10 - 2.2.1 - 2.2.1 - 0.9.5.Final + 2.3.1 + 2.3.1 + 1.0.0.Final 3.4.14 - 2.12.8 - 1.21 + + 2.12.9 + 1.22 1.1.0 2.2.5 1.3.1 @@ -148,31 +147,36 @@ 1.6.5 1.0.4 5.5.1.201910021850-r - 6.0.6 - 1.0.5 - 4.0.0-beta03 + 6.1.4 + 1.0.6 + 4.0.0 3.10.2 1.11.0 - 2.10.0 + 2.10.1 3.12.6 + 1.7.28 3.1.0 3.1.7 0.1.0 - 0.5.1 - 4.6.4 + 0.6.1 + 4.7.0 0.19.1 2.2.0 2.24.1 5.1.8.RELEASE 2.1.9.RELEASE 5.2.0.RELEASE + 2.1.10.RELEASE 2.4.4.Final - 3.0.0 + 3.2.4 5.3.1 4.7.2 - 1.0.0.Final - 7.0.1 + 1.0.1.Final + 8.0.1 + 1.14.0 + 0.1.55 + 1.1.1 @@ -301,11 +305,21 @@ quarkus-artemis-core ${project.version} + + io.quarkus + quarkus-test-artemis + ${project.version} + io.quarkus quarkus-artemis-jms ${project.version} + + io.quarkus + quarkus-config-yaml + ${project.version} + io.quarkus quarkus-elasticsearch-rest-client @@ -316,6 +330,11 @@ quarkus-security ${project.version} + + io.quarkus + quarkus-elytron-security-common + ${project.version} + io.quarkus quarkus-elytron-security @@ -622,6 +641,11 @@ quarkus-spring-data-jpa ${project.version} + + io.quarkus + quarkus-spring-boot-properties + ${project.version} + io.quarkus quarkus-swagger-ui @@ -687,6 +711,11 @@ quarkus-jgit ${project.version} + + io.quarkus + quarkus-jsch + ${project.version} + io.quarkus quarkus-narayana-stm @@ -697,6 +726,11 @@ quarkus-elytron-security-jdbc ${project.version} + + io.quarkus + quarkus-elytron-security-ldap + ${project.version} + io.quarkus quarkus-vault @@ -707,6 +741,26 @@ quarkus-vault-spi ${project.version} + + io.quarkus + quarkus-test-vault + ${project.version} + + + io.quarkus + quarkus-logging-json + ${project.version} + + + io.quarkus + quarkus-logging-sentry + ${project.version} + + + io.quarkus + quarkus-logging-gelf + ${project.version} + @@ -724,6 +778,11 @@ quarkus-test-h2 ${project.version} + + io.quarkus + quarkus-test-ldap + ${project.version} + io.quarkus quarkus-test-derby @@ -734,6 +793,16 @@ quarkus-test-kubernetes-client ${project.version} + + io.quarkus + quarkus-qute + ${project.version} + + + io.quarkus + quarkus-resteasy-qute + ${project.version} + @@ -799,6 +868,11 @@ + + biz.paluch.logging + logstash-gelf + ${logstash-gelf.version} + org.apache.commons commons-lang3 @@ -1068,8 +1142,22 @@ ${quarkus-security.version} + io.smallrye smallrye-config + + 1.5.0 + + + + io.smallrye + smallrye-config-common + + 1.5.0 + + + io.smallrye.config + smallrye-config ${smallrye-config.version} @@ -1078,6 +1166,16 @@ + + io.smallrye.config + smallrye-config-common + ${smallrye-config.version} + + + io.smallrye.config + smallrye-config-source-yaml + ${smallrye-config.version} + io.smallrye smallrye-health @@ -1141,6 +1239,11 @@ smallrye-fault-tolerance ${smallrye-fault-tolerance.version} + + io.smallrye + smallrye-fault-tolerance-context-propagation + ${smallrye-fault-tolerance.version} + io.smallrye smallrye-context-propagation @@ -1225,6 +1328,14 @@ jakarta.interceptor jakarta.interceptor-api ${jakarta.interceptor-api.version} + + + + + jakarta.ejb + jakarta.ejb-api + + org.jboss.spec.javax.xml.bind @@ -1251,6 +1362,11 @@ artemis-jms-client ${artemis.version} + + org.apache.activemq + artemis-commons + ${artemis.version} + org.apache.httpcomponents httpclient @@ -1327,7 +1443,11 @@ okhttp ${okhttp.version} - + + io.sentry + sentry + ${sentry.version} + org.apache.maven.plugin-tools maven-plugin-annotations @@ -1367,7 +1487,18 @@ org.mongodb - mongo-java-driver + mongodb-driver-sync + ${mongo-client.version} + + + org.mongodb + mongodb-driver-async + ${mongo-client.version} + + + + org.mongodb + mongodb-driver-legacy ${mongo-client.version} @@ -1396,7 +1527,11 @@ junit-jupiter ${test-containers.version} - + + org.testcontainers + postgresql + ${test-containers.version} + org.codehaus.plexus plexus-utils @@ -1407,6 +1542,16 @@ plexus-component-annotations ${plexus-component-annotations.version} + + com.jcraft + jsch + ${jsch.version} + + + com.jcraft + jzlib + ${jzlib.version} + org.eclipse.jgit org.eclipse.jgit @@ -1730,6 +1875,11 @@ wildfly-elytron-realm-jdbc ${wildfly-elytron.version} + + org.wildfly.security + wildfly-elytron-realm-ldap + ${wildfly-elytron.version} + org.wildfly.security wildfly-elytron-ssl @@ -1765,6 +1915,11 @@ wildfly-elytron-x500-cert ${wildfly-elytron.version} + + org.wildfly.security + wildfly-elytron-credential + ${wildfly-elytron.version} + org.ow2.asm @@ -1941,12 +2096,12 @@ io.vertx vertx-web - ${vertx-web.version} + ${vertx.version} io.vertx vertx-web-common - ${vertx-web.version} + ${vertx.version} io.smallrye.reactive @@ -2193,7 +2348,7 @@ org.jetbrains.kotlin - kotlin-compiler-embeddable + kotlin-compiler ${kotlin.version} @@ -2216,6 +2371,20 @@ com.amazonaws.serverless aws-serverless-java-container-core ${aws-lambda-serverless-java-container.version} + + + commons-logging + commons-logging + + + javax.servlet + javax.servlet-api + + + javax.ws.rs + javax.ws.rs-api + + @@ -2424,6 +2593,23 @@ + + + org.springframework.boot + spring-boot + ${spring-boot.version} + + + org.springframework + spring-core + + + org.springframework + spring-context + + + + org.keycloak @@ -2450,6 +2636,23 @@ quarkus-vertx-graphql ${project.version} + + + + io.quarkus.qute + qute-core + ${project.version} + + + io.quarkus.qute + qute-generator + ${project.version} + + + io.quarkus.qute + qute-rxjava + ${project.version} + diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 768a32d5591d5..e298642099312 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -16,14 +16,14 @@ - 6.14 - 7.4.0 + 6.15 + 7.5.0 4.1.1 3.8.1 - 1.3.21 + 1.3.41 2.12.8 4.1.1 @@ -35,11 +35,11 @@ 19.2.1 4.1.1 0.0.9 - 3.8.3 - 2.10.0 + 3.8.4 + 2.10.1 - 2.3.28 + 2.3.29 1.5.0.Final @@ -55,13 +55,13 @@ 3.6.2 0.7.6 - 5.6.4 + 6.0.1 2.1 1.3 - 2.2 - 2.0.2 + 2.2.1 + 2.0.3 1.0 1.3.4 1.1.2 @@ -79,7 +79,9 @@ - quay.io/keycloak/keycloak:7.0.1 + quay.io/keycloak/keycloak:8.0.1 + + 4.0.13 @@ -153,6 +155,11 @@ wagon-provider-api ${wagon-provider-api.version} + + com.unboundid + unboundid-ldapsdk + ${unboundid-ldap.version} + @@ -286,7 +293,7 @@ io.fabric8 docker-maven-plugin - 0.29.0 + 0.31.0 maven-javadoc-plugin @@ -299,7 +306,7 @@ org.jboss.jandex jandex-maven-plugin - 1.0.6 + 1.0.7 net.revelc.code.formatter diff --git a/ci-templates/jvm-build-steps.yaml b/ci-templates/jvm-build-steps.yaml index 0cde1f96232cd..0963818a9c2dd 100644 --- a/ci-templates/jvm-build-steps.yaml +++ b/ci-templates/jvm-build-steps.yaml @@ -17,7 +17,10 @@ steps: - script: docker run --rm --publish 8000:8000 --name build-dynamodb -d amazon/dynamodb-local:1.11.477 displayName: 'start dynamodb' - + +- script: docker run --rm --publish 7687:7687 --name build-neo4j -e NEO4J_AUTH=neo4j/secret -e NEO4J_dbms_memory_pagecache_size=10M -e NEO4J_dbms_memory_heap_initial__size=10M -d neo4j/neo4j-experimental:4.0.0-rc01 + displayName: 'start neo4j' + - bash: | sudo service mysql stop || true docker run --rm --publish 3306:3306 --name build-mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_RANDOM_ROOT_PASSWORD=true -e MYSQL_DATABASE=hibernate_orm_test -d mysql:5 --skip-ssl @@ -29,5 +32,5 @@ steps: goals: 'install' mavenOptions: $(MAVEN_OPTS) jdkVersionOption: ${{ parameters.jdk }} - options: '-B --settings azure-mvn-settings.xml -Dnative-image.docker-build -Dtest-postgresql -Dtest-elasticsearch -Dtest-mysql -Dtest-dynamodb -Dtest-vault -Dno-format ${{ parameters.extraf }}' + options: '-B --settings azure-mvn-settings.xml -Dnative-image.docker-build -Dtest-postgresql -Dtest-elasticsearch -Dtest-mysql -Dtest-dynamodb -Dtest-vault -Dtest-neo4j -Dno-format ${{ parameters.extra }}' diff --git a/ci-templates/native-build-steps.yaml b/ci-templates/native-build-steps.yaml index 68c9206056df9..b4b3a8efbb45d 100644 --- a/ci-templates/native-build-steps.yaml +++ b/ci-templates/native-build-steps.yaml @@ -7,6 +7,7 @@ parameters: dynamodb: false keycloak: false mysql: false + neo4j: false timeoutInMinutes: 80 @@ -14,8 +15,8 @@ jobs: - job: ${{ parameters.name }} condition: and(eq(variables.LINUX_USE_VMS, ${{parameters.expectUseVMs}}),succeeded()) displayName: ${{ join(', ', parameters.modules) }} - timeoutInMinutes: ${{ parameters.timeoutInMinutes }} - pool: ${{ parameters.poolSettings }} + timeoutInMinutes: ${{parameters.timeoutInMinutes}} + pool: ${{parameters.poolSettings}} workspace: clean: all @@ -25,10 +26,11 @@ jobs: steps: - script: docker rm -f $(docker ps -qa) || true displayName: 'Docker Cleanup' - - task: DownloadPipelineArtifact@2 + - task: DownloadPipelineArtifact@1 inputs: artifact: $(Build.SourceVersion)-BuiltMavenRepo - path: $(Pipeline.Workspace)/.m2/repository/ + targetPath: $(Pipeline.Workspace)/.m2/repository/ + - bash: (cd $(Pipeline.Workspace)/.m2/repository/$(Build.SourceVersion)-BuiltMavenRepo/; mv * ..) - ${{ if eq(parameters.postgres, 'true') }}: - script: docker run --rm --publish 5432:5432 --name build-postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -d postgres:10.5 displayName: 'start postgres' @@ -36,8 +38,11 @@ jobs: - script: docker run --rm --publish 8000:8000 --name build-dynamodb -d amazon/dynamodb-local:1.11.477 displayName: 'start dynamodb' - ${{ if eq(parameters.keycloak, 'true') }}: - - script: docker run --rm --publish 8180:8080 --name build-keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e JAVA_OPTS="-server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djava.awt.headless=true -Dkeycloak.profile.feature.upload_scripts=enabled" -d quay.io/keycloak/keycloak:7.0.1 + - script: docker run --rm --publish 8180:8080 --name build-keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e JAVA_OPTS="-server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djava.awt.headless=true -Dkeycloak.profile.feature.upload_scripts=enabled" -d quay.io/keycloak/keycloak:8.0.1 displayName: 'start keycloak' + - ${{ if eq(parameters.neo4j, 'true') }}: + - script: docker run --rm --publish 7687:7687 --name build-neo4j -e NEO4J_AUTH=neo4j/secret -e NEO4J_dbms_memory_pagecache_size=10M -e NEO4J_dbms_memory_heap_initial__size=10M -d neo4j/neo4j-experimental:4.0.0-rc01 + displayName: 'start neo4j' - ${{ if eq(parameters.mysql, 'true') }}: - bash: | sudo service mysql stop || true @@ -49,4 +54,4 @@ jobs: inputs: goals: 'install' mavenOptions: $(MAVEN_OPTS) - options: '-pl integration-tests/${{ join('',integration-tests/'', parameters.modules) }} -B --settings azure-mvn-settings.xml -Dquarkus.native.container-build=true -Dtest-postgresql -Dtest-elasticsearch -Dtest-keycloak -Ddocker-keycloak -Dtest-dynamodb -Dtest-mysql -Dtest-vault -Dnative-image.xmx=6g -Dnative -Dno-format' + options: '-pl integration-tests/${{ join('',integration-tests/'', parameters.modules) }} -B --settings azure-mvn-settings.xml -Dquarkus.native.container-build=true -Dtest-postgresql -Dtest-elasticsearch -Dtest-keycloak -Ddocker-keycloak -Dtest-dynamodb -Dtest-mysql -Dtest-vault -Dtest-neo4j -Dnative-image.xmx=6g -Dnative -Dno-format' diff --git a/ci-templates/prepare-cache.yaml b/ci-templates/prepare-cache.yaml new file mode 100644 index 0000000000000..90557f8c0fc08 --- /dev/null +++ b/ci-templates/prepare-cache.yaml @@ -0,0 +1,6 @@ +steps: +# We remove all the Quarkus artifacts before pushing the $(MAVEN_CACHE_FOLDER) to the cache. +# We don't remove Quarkus HTTP, Gizmo and Quarkus Security as they are external to this build. +- script: find $(MAVEN_CACHE_FOLDER)io/quarkus/ -mindepth 1 -maxdepth 1 \( ! -name http -a ! -name gizmo -a ! -name security \) -exec echo {} \; -exec rm -rf {} \; + displayName: 'Remove Quarkus artifacts before caching' + diff --git a/ci-templates/stages.yml b/ci-templates/stages.yml index 31fe1ac3153c3..881f54b819e79 100644 --- a/ci-templates/stages.yml +++ b/ci-templates/stages.yml @@ -32,7 +32,8 @@ stages: - task: CacheBeta@0 inputs: - key: maven | bom/runtime/pom.xml #if we attempt to use all poms then when they get copied to target everything breaks. This should be good enough, it does not need to be perfect + # the number below is a cache version when we want to force a cache refresh + key: maven | "2" | bom/runtime/pom.xml #if we attempt to use all poms then when they get copied to target everything breaks. This should be good enough, it does not need to be perfect path: $(MAVEN_CACHE_FOLDER) securityNamespace: cache cacheHitVar: CACHE_RESTORED @@ -42,10 +43,12 @@ stages: displayName: 'Maven Build' condition: and(ne(variables.CACHE_RESTORED, 'true'), succeeded()) inputs: - goals: 'package' + goals: 'install' mavenOptions: $(MAVEN_OPTS) options: '-B --settings azure-mvn-settings.xml -DskipTests=true -Dno-format -DskipDocs' + - template: prepare-cache.yaml + - job: Cache_Windows_Maven_Repo #windows has different line endings so the cache key is different displayName: 'Windows Maven Repo' condition: and(eq(variables.LINUX_USE_VMS, ${{parameters.expectUseVMs}}),succeeded()) @@ -59,7 +62,8 @@ stages: steps: - task: CacheBeta@0 inputs: - key: mavenWindows | bom/runtime/pom.xml #if we attempt to use all poms then when they get copied to target everything breaks. This should be good enough, it does not need to be perfect + # the number below is a cache version when we want to force a cache refresh + key: mavenWindows | "2" | bom/runtime/pom.xml #if we attempt to use all poms then when they get copied to target everything breaks. This should be good enough, it does not need to be perfect path: $(MAVEN_CACHE_FOLDER) securityNamespace: cache cacheHitVar: CACHE_RESTORED @@ -69,7 +73,7 @@ stages: displayName: 'Maven Build' condition: and(ne(variables.CACHE_RESTORED, 'true'), succeeded()) inputs: - goals: 'package' + goals: 'install' mavenOptions: $(MAVEN_OPTS) options: '-B --settings azure-mvn-settings.xml -DskipTests=true -Dno-format -DskipDocs' @@ -81,7 +85,7 @@ stages: - job: Build_JDK8_Linux displayName: 'Build JDK8 Linux' condition: and(eq(variables.LINUX_USE_VMS, ${{parameters.expectUseVMs}}),succeeded()) - timeoutInMinutes: 60 + timeoutInMinutes: 90 pool: vmImage: 'Ubuntu 16.04' @@ -90,8 +94,12 @@ stages: steps: - template: jvm-build-steps.yaml + parameters: + # we only generate the PDF documentation once for the initial JDK 8 build + extra: '-Ddocumentation-pdf' - publish: $(MAVEN_CACHE_FOLDER) artifact: $(Build.SourceVersion)-BuiltMavenRepo + - template: prepare-cache.yaml - stage: run_jvm_tests_stage${{parameters.expectUseVMs}} displayName: '${{parameters.displayPrefix}} Run JVM Tests' @@ -100,7 +108,7 @@ stages: - job: Windows_Build displayName: 'Windows JVM Build' condition: and(eq(variables.LINUX_USE_VMS, ${{parameters.expectUseVMs}}),succeeded()) - timeoutInMinutes: 60 + timeoutInMinutes: 90 pool: # Always use hosted pool for windows vmImage: 'vs2017-win2016' @@ -122,7 +130,7 @@ stages: options: '-B --settings azure-mvn-settings.xml -Dno-native -Dno-format' - job: Build_JDK11_Linux - timeoutInMinutes: 60 + timeoutInMinutes: 90 condition: and(eq(variables.LINUX_USE_VMS, ${{parameters.expectUseVMs}}),succeeded()) displayName: 'Linux JDK11 Build' pool: ${{parameters.poolSettings}} @@ -133,9 +141,10 @@ stages: - template: jvm-build-steps.yaml parameters: jdk: '1.11' + - template: prepare-cache.yaml - job: Build_JDK12_Linux - timeoutInMinutes: 60 + timeoutInMinutes: 90 condition: and(eq(variables.LINUX_USE_VMS, ${{parameters.expectUseVMs}}),succeeded()) displayName: 'Linux JDK12 Build' pool: ${{parameters.poolSettings}} @@ -147,6 +156,7 @@ stages: - template: jvm-build-steps.yaml parameters: jdk: '1.12' + - template: prepare-cache.yaml - job: Run_TCKs condition: and(eq(variables.LINUX_USE_VMS, ${{parameters.expectUseVMs}}),succeeded()) @@ -175,8 +185,7 @@ stages: mavenOptions: $(MAVEN_OPTS) options: '-B --settings azure-mvn-settings.xml' mavenPomFile: 'tcks/pom.xml' - - + - template: prepare-cache.yaml - stage: run_native_tests_stage${{parameters.expectUseVMs}} displayName: '${{parameters.displayPrefix}} Native Tests' @@ -194,7 +203,7 @@ stages: parameters: poolSettings: ${{parameters.poolSettings}} expectUseVMs: ${{parameters.expectUseVMs}} - timeoutInMinutes: 25 + timeoutInMinutes: 30 modules: - jpa-h2 - jpa-mariadb @@ -233,11 +242,12 @@ stages: parameters: poolSettings: ${{parameters.poolSettings}} expectUseVMs: ${{parameters.expectUseVMs}} - timeoutInMinutes: 20 + timeoutInMinutes: 30 modules: - artemis-core - artemis-jms - kafka + - kafka-streams name: messaging - template: native-build-steps.yaml @@ -249,6 +259,7 @@ stages: - elytron-security-oauth2 - elytron-security - elytron-security-jdbc + - elytron-security-ldap - elytron-undertow name: security_1 keycloak: true @@ -262,6 +273,7 @@ stages: - elytron-resteasy - oidc - oidc-code-flow + - oidc-tenancy - vault-app - keycloak-authorization name: security_2 @@ -289,6 +301,7 @@ stages: - mongodb-panache - neo4j name: data_4 + neo4j: true - template: native-build-steps.yaml parameters: @@ -304,11 +317,12 @@ stages: parameters: poolSettings: ${{parameters.poolSettings}} expectUseVMs: ${{parameters.expectUseVMs}} - timeoutInMinutes: 25 + timeoutInMinutes: 30 modules: - resteasy-jackson - vertx - vertx-http + - vertx-graphql - virtual-http name: http @@ -316,10 +330,12 @@ stages: parameters: poolSettings: ${{parameters.poolSettings}} expectUseVMs: ${{parameters.expectUseVMs}} - timeoutInMinutes: 25 + timeoutInMinutes: 40 modules: + - maven - jackson - jsonb + - jsch - jgit name: misc_1 @@ -338,10 +354,11 @@ stages: parameters: poolSettings: ${{parameters.poolSettings}} expectUseVMs: ${{parameters.expectUseVMs}} - timeoutInMinutes: 25 + timeoutInMinutes: 35 modules: - kogito - kubernetes-client + - quartz name: misc_3 - template: native-build-steps.yaml @@ -353,6 +370,7 @@ stages: - spring-di - spring-web - spring-data-jpa + - spring-boot-properties name: spring - template: native-build-steps.yaml diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildChainBuilder.java b/core/builder/src/main/java/io/quarkus/builder/BuildChainBuilder.java index e9d41f5001f2f..d99f28909b296 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildChainBuilder.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildChainBuilder.java @@ -22,8 +22,6 @@ import org.wildfly.common.Assert; import io.quarkus.builder.item.BuildItem; -import io.quarkus.builder.item.NamedBuildItem; -import io.quarkus.builder.item.SymbolicBuildItem; /** * A build chain builder. @@ -105,23 +103,7 @@ public BuildStepBuilder addBuildStep() { */ public BuildChainBuilder addInitial(Class type) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } - initialIds.add(new ItemId(type, null)); - return this; - } - - public BuildChainBuilder addInitial(Enum symbolic) { - Assert.checkNotNullParam("symbolic", symbolic); - initialIds.add(new ItemId(SymbolicBuildItem.class, symbolic)); - return this; - } - - public BuildChainBuilder addInitial(Class> type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - initialIds.add(new ItemId(type, name)); + initialIds.add(new ItemId(type)); return this; } @@ -143,23 +125,7 @@ public BuildChainBuilder loadProviders(ClassLoader classLoader) throws ChainBuil */ public BuildChainBuilder addFinal(Class type) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot consume a named build item without a name"); - } - finalIds.add(new ItemId(type, null)); - return this; - } - - public BuildChainBuilder addFinal(Enum symbolic) { - Assert.checkNotNullParam("symbolic", symbolic); - finalIds.add(new ItemId(SymbolicBuildItem.class, symbolic)); - return this; - } - - public BuildChainBuilder addFinal(Class> type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - finalIds.add(new ItemId(type, name)); + finalIds.add(new ItemId(type)); return this; } diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildContext.java b/core/builder/src/main/java/io/quarkus/builder/BuildContext.java index d86ff32f763d4..f787bac7f64d2 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildContext.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildContext.java @@ -15,8 +15,6 @@ import io.quarkus.builder.diag.Diagnostic; import io.quarkus.builder.item.BuildItem; import io.quarkus.builder.item.MultiBuildItem; -import io.quarkus.builder.item.NamedBuildItem; -import io.quarkus.builder.item.NamedMultiBuildItem; import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.builder.location.Location; @@ -57,10 +55,7 @@ public String getBuildTargetName() { */ public void produce(BuildItem item) { Assert.checkNotNullParam("item", item); - if (item instanceof NamedBuildItem) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } - doProduce(new ItemId(item.getClass(), null), item); + doProduce(new ItemId(item.getClass()), item); } /** @@ -72,7 +67,7 @@ public void produce(BuildItem item) { public void produce(List items) { Assert.checkNotNullParam("items", items); for (MultiBuildItem item : items) { - doProduce(new ItemId(item.getClass(), null), item); + doProduce(new ItemId(item.getClass()), item); } } @@ -88,42 +83,7 @@ public void produce(List items) { */ public void produce(Class type, T item) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } - doProduce(new ItemId(type, null), type.cast(item)); - } - - /** - * Produce the given item. If the {@code type} refers to a item which is declared with multiplicity, then this - * method can be called more than once for the given {@code type}, otherwise it must be called no more than once. - * - * @param name the build item name (must not be {@code null}) - * @param item the item value (must not be {@code null}) - * @throws IllegalArgumentException if the item does not allow multiplicity but this method is called more than one time, - * or if the type of item could not be determined - */ - public void produce(N name, NamedBuildItem item) { - Assert.checkNotNullParam("name", name); - Assert.checkNotNullParam("item", item); - doProduce(new ItemId(item.getClass(), name), item); - } - - /** - * Produce the given item. If the {@code type} refers to a item which is declared with multiplicity, then this - * method can be called more than once for the given {@code type}, otherwise it must be called no more than once. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @param item the item value (must not be {@code null}) - * @throws IllegalArgumentException if the item does not allow multiplicity but this method is called more than one time, - * or if the type of item could not be determined - */ - public > void produce(Class type, N name, NamedBuildItem item) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - Assert.checkNotNullParam("item", item); - doProduce(new ItemId(type, name), item); + doProduce(new ItemId(type), type.cast(item)); } /** @@ -140,33 +100,7 @@ public T consume(Class type) { if (!running) { throw Messages.msg.buildStepNotRunning(); } - final ItemId id = new ItemId(type, null); - if (id.isMulti()) { - throw Messages.msg.cannotMulti(id); - } - if (!stepInfo.getConsumes().contains(id)) { - throw Messages.msg.undeclaredItem(id); - } - return type.cast(execution.getSingles().get(id)); - } - - /** - * Consume the value produced for the named item. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @return the produced item (may be {@code null}) - * @throws IllegalArgumentException if this deployer was not declared to consume {@code type}, or if {@code type} is - * {@code null} - * @throws ClassCastException if the cast failed - */ - public > T consume(Class type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - if (!running) { - throw Messages.msg.buildStepNotRunning(); - } - final ItemId id = new ItemId(type, name); + final ItemId id = new ItemId(type); if (id.isMulti()) { throw Messages.msg.cannotMulti(id); } @@ -191,35 +125,7 @@ public List consumeMulti(Class type) { if (!running) { throw Messages.msg.buildStepNotRunning(); } - final ItemId id = new ItemId(type, null); - if (!id.isMulti()) { - // can happen if obj changes base class - throw Messages.msg.cannotMulti(id); - } - if (!stepInfo.getConsumes().contains(id)) { - throw Messages.msg.undeclaredItem(id); - } - return new ArrayList<>((List) (List) execution.getMultis().getOrDefault(id, Collections.emptyList())); - } - - /** - * Consume all of the values produced for the named item. If the - * item type implements {@link Comparable}, it will be sorted by natural order before return. The returned list - * is a mutable copy. - * - * @param type the item element type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @return the produced items (may be empty, will not be {@code null}) - * @throws IllegalArgumentException if this deployer was not declared to consume {@code type}, or if {@code type} is - * {@code null} - */ - public > List consumeMulti(Class type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - if (!running) { - throw Messages.msg.buildStepNotRunning(); - } - final ItemId id = new ItemId(type, name); + final ItemId id = new ItemId(type); if (!id.isMulti()) { // can happen if obj changes base class throw Messages.msg.cannotMulti(id); @@ -246,22 +152,6 @@ public List consumeMulti(Class type, Comparator return result; } - /** - * Consume all of the values produced for the named item, re-sorting it according - * to the given comparator. The returned list is a mutable copy. - * - * @param type the item element type (must not be {@code null}) - * @param comparator the comparator to use (must not be {@code null}) - * @return the produced items (may be empty, will not be {@code null}) - * @throws IllegalArgumentException if this deployer was not declared to consume {@code type}, or if {@code type} is - * {@code null} - */ - public > List consumeMulti(Class type, N name, Comparator comparator) { - final List result = consumeMulti(type, name); - result.sort(comparator); - return result; - } - /** * Determine if a item was produced and is therefore available to be {@linkplain #consume(Class) consumed}. * @@ -270,21 +160,7 @@ public > List consumeMulti(Class type, * not consume the named item */ public boolean isAvailableToConsume(Class type) { - final ItemId id = new ItemId(type, null); - return stepInfo.getConsumes().contains(id) && id.isMulti() - ? !execution.getMultis().getOrDefault(id, Collections.emptyList()).isEmpty() - : execution.getSingles().containsKey(id); - } - - /** - * Determine if a item was produced and is therefore available to be {@linkplain #consume(Class) consumed}. - * - * @param type the item type (must not be {@code null}) - * @return {@code true} if the item was produced and is available, {@code false} if it was not or if this deployer does - * not consume the named item - */ - public boolean isAvailableToConsume(Class> type, N name) { - final ItemId id = new ItemId(type, name); + final ItemId id = new ItemId(type); return stepInfo.getConsumes().contains(id) && id.isMulti() ? !execution.getMultis().getOrDefault(id, Collections.emptyList()).isEmpty() : execution.getSingles().containsKey(id); @@ -299,19 +175,7 @@ public boolean isAvailableToConsume(Class> type, * not produce the named item */ public boolean isConsumed(Class type) { - return execution.getBuildChain().getConsumed().contains(new ItemId(type, null)); - } - - /** - * Determine if a item will be consumed in this build. If a item is not consumed, then build steps are not - * required to produce it. - * - * @param type the item type (must not be {@code null}) - * @return {@code true} if the item will be consumed, {@code false} if it will not be or if this deployer does - * not produce the named item - */ - public boolean isConsumed(Class> type, N name) { - return execution.getBuildChain().getConsumed().contains(new ItemId(type, name)); + return execution.getBuildChain().getConsumed().contains(new ItemId(type)); } /** diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildExecutionBuilder.java b/core/builder/src/main/java/io/quarkus/builder/BuildExecutionBuilder.java index 59a9a0f9d233a..a2303b83993bd 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildExecutionBuilder.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildExecutionBuilder.java @@ -9,7 +9,6 @@ import org.wildfly.common.Assert; import io.quarkus.builder.item.BuildItem; -import io.quarkus.builder.item.NamedBuildItem; /** * A builder for a deployer execution. @@ -49,10 +48,7 @@ public String getBuildTargetName() { */ public BuildExecutionBuilder produce(T item) { Assert.checkNotNullParam("item", item); - if (item instanceof NamedBuildItem) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } - produce(new ItemId(item.getClass(), null), item); + produce(new ItemId(item.getClass()), item); return this; } @@ -69,45 +65,7 @@ public BuildExecutionBuilder produce(T item) { public BuildExecutionBuilder produce(Class type, T item) { Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("item", item); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } - produce(new ItemId(type, null), item); - return this; - } - - /** - * Provide an initial item. - * - * @param name the build item name (must not be {@code null}) - * @param item the item value - * @return this builder - * @throws IllegalArgumentException if this deployer chain was not declared to initially produce {@code type}, - * or if the item does not allow multiplicity but this method is called more than one time - */ - public > BuildExecutionBuilder produce(N name, T item) { - Assert.checkNotNullParam("name", name); - Assert.checkNotNullParam("item", item); - produce(new ItemId(item.getClass(), name), item); - return this; - } - - /** - * Provide an initial item. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @param item the item value - * @return this builder - * @throws IllegalArgumentException if this deployer chain was not declared to initially produce {@code type}, - * or if {@code type} is {@code null}, or if the item does not allow multiplicity but this method is called - * more than one time - */ - public > BuildExecutionBuilder produce(Class type, N name, T item) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - Assert.checkNotNullParam("item", item); - produce(new ItemId(type, name), item); + produce(new ItemId(type), item); return this; } diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildResult.java b/core/builder/src/main/java/io/quarkus/builder/BuildResult.java index c37b132a8ef4d..2219e2dbd6f02 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildResult.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildResult.java @@ -42,7 +42,7 @@ public final class BuildResult { * @throws ClassCastException if the cast failed */ public T consume(Class type) { - final ItemId itemId = new ItemId(type, null); + final ItemId itemId = new ItemId(type); final Object item = simpleItems.get(itemId); if (item == null) { throw Messages.msg.undeclaredItem(itemId); @@ -58,7 +58,7 @@ public T consume(Class type) { * @throws ClassCastException if the cast failed */ public T consumeOptional(Class type) { - final ItemId itemId = new ItemId(type, null); + final ItemId itemId = new ItemId(type); final Object item = simpleItems.get(itemId); if (item == null) { return null; @@ -74,7 +74,7 @@ public T consumeOptional(Class type) { * @throws IllegalArgumentException if this deployer was not declared to consume {@code type} */ public List consumeMulti(Class type) { - final ItemId itemId = new ItemId(type, null); + final ItemId itemId = new ItemId(type); @SuppressWarnings("unchecked") final List items = (List) (List) multiItems.get(itemId); if (items == null) { diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java b/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java index b19f09d9162e4..37ca7451a0b4a 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java @@ -9,8 +9,6 @@ import io.quarkus.builder.item.BuildItem; import io.quarkus.builder.item.EmptyBuildItem; -import io.quarkus.builder.item.NamedBuildItem; -import io.quarkus.builder.item.SymbolicBuildItem; /** * A builder for build step instances within a chain. A build step can consume and produce items. It may also register @@ -47,10 +45,7 @@ public BuildStepBuilder setBuildStep(final BuildStep buildStep) { */ public BuildStepBuilder beforeConsume(Class type) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot consume a named build item without a name"); - } - addProduces(new ItemId(type, null), Constraint.ORDER_ONLY, ProduceFlags.NONE); + addProduces(new ItemId(type), Constraint.ORDER_ONLY, ProduceFlags.NONE); return this; } @@ -65,42 +60,7 @@ public BuildStepBuilder beforeConsume(Class type) { public BuildStepBuilder beforeConsume(Class type, ProduceFlag flag) { Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("flag", flag); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot consume a named build item without a name"); - } - addProduces(new ItemId(type, null), Constraint.ORDER_ONLY, ProduceFlags.of(flag)); - return this; - } - - /** - * This build step should complete before any build steps which consume the given item {@code type} are initiated. - * If no such build steps exist, no ordering constraint is enacted. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder beforeConsume(Class> type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - addProduces(new ItemId(type, name), Constraint.ORDER_ONLY, ProduceFlags.NONE); - return this; - } - - /** - * This build step should complete before any build steps which consume the given item {@code type} are initiated. - * If no such build steps exist, no ordering constraint is enacted. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @param flag the producer flag to apply (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder beforeConsume(Class> type, N name, ProduceFlag flag) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - Assert.checkNotNullParam("flag", flag); - addProduces(new ItemId(type, name), Constraint.ORDER_ONLY, ProduceFlags.of(flag)); + addProduces(new ItemId(type), Constraint.ORDER_ONLY, ProduceFlags.of(flag)); return this; } @@ -113,25 +73,7 @@ public BuildStepBuilder beforeConsume(Class> typ */ public BuildStepBuilder afterProduce(Class type) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } - addConsumes(new ItemId(type, null), Constraint.ORDER_ONLY, ConsumeFlags.of(ConsumeFlag.OPTIONAL)); - return this; - } - - /** - * This build step should be initiated after any build steps which produce the given item {@code type} are completed. - * If no such build steps exist, no ordering constraint is enacted. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder afterProduce(Class> type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - addConsumes(new ItemId(type, name), Constraint.ORDER_ONLY, ConsumeFlags.of(ConsumeFlag.OPTIONAL)); + addConsumes(new ItemId(type), Constraint.ORDER_ONLY, ConsumeFlags.of(ConsumeFlag.OPTIONAL)); return this; } @@ -145,13 +87,10 @@ public BuildStepBuilder afterProduce(Class> type */ public BuildStepBuilder produces(Class type) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } if (EmptyBuildItem.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Cannot produce an empty build item"); } - addProduces(new ItemId(type, null), Constraint.REAL, ProduceFlags.NONE); + addProduces(new ItemId(type), Constraint.REAL, ProduceFlags.NONE); return this; } @@ -167,13 +106,10 @@ public BuildStepBuilder produces(Class type) { public BuildStepBuilder produces(Class type, ProduceFlag flag) { Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("flag", flag); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } if (EmptyBuildItem.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Cannot produce an empty build item"); } - addProduces(new ItemId(type, null), Constraint.REAL, ProduceFlags.of(flag)); + addProduces(new ItemId(type), Constraint.REAL, ProduceFlags.of(flag)); return this; } @@ -190,13 +126,10 @@ public BuildStepBuilder produces(Class type, ProduceFlag fl public BuildStepBuilder produces(Class type, ProduceFlag flag1, ProduceFlag flag2) { Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("flag", flag1); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } if (EmptyBuildItem.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Cannot produce an empty build item"); } - addProduces(new ItemId(type, null), Constraint.REAL, ProduceFlags.of(flag1).with(flag2)); + addProduces(new ItemId(type), Constraint.REAL, ProduceFlags.of(flag1).with(flag2)); return this; } @@ -212,73 +145,10 @@ public BuildStepBuilder produces(Class type, ProduceFlag fl public BuildStepBuilder produces(Class type, ProduceFlags flags) { Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("flag", flags); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce a named build item without a name"); - } if (EmptyBuildItem.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Cannot produce an empty build item"); } - addProduces(new ItemId(type, null), Constraint.REAL, flags); - return this; - } - - /** - * Similarly to {@link #beforeConsume(Class)}, establish that this build step must come before the consumer(s) of the - * given item {@code type}; however, only one {@code producer} may exist for the given item. In addition, the - * build step may produce an actual value for this item, which will be shared to all consumers during deployment. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder produces(Class> type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - addProduces(new ItemId(type, name), Constraint.REAL, ProduceFlags.NONE); - return this; - } - - /** - * Similarly to {@link #beforeConsume(Class)}, establish that this build step must come before the consumer(s) of the - * given item {@code type}; however, only one {@code producer} may exist for the given item. In addition, the - * build step may produce an actual value for this item, which will be shared to all consumers during deployment. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @param flag the producer flag to apply (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder produces(Class> type, N name, ProduceFlag flag) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - Assert.checkNotNullParam("flag", flag); - addProduces(new ItemId(type, name), Constraint.REAL, ProduceFlags.of(flag)); - return this; - } - - /** - * Declare that the build step "produces" an empty item with the given identifier. - * - * @param symbolic the item identifier (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder beforeVirtual(Enum symbolic) { - Assert.checkNotNullParam("symbolic", symbolic); - addProduces(new ItemId(SymbolicBuildItem.class, symbolic), Constraint.ORDER_ONLY, ProduceFlags.NONE); - return this; - } - - /** - * Declare that the build step "produces" a virtual item with the given identifier. - * - * @param symbolic the item identifier (must not be {@code null}) - * @param flag the producer flag to apply (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder beforeVirtual(Enum symbolic, ProduceFlag flag) { - Assert.checkNotNullParam("symbolic", symbolic); - Assert.checkNotNullParam("flag", flag); - addProduces(new ItemId(SymbolicBuildItem.class, symbolic), Constraint.ORDER_ONLY, ProduceFlags.of(flag)); + addProduces(new ItemId(type), Constraint.REAL, flags); return this; } @@ -291,28 +161,10 @@ public BuildStepBuilder beforeVirtual(Enum symbolic, ProduceFlag flag) { */ public BuildStepBuilder consumes(Class type) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot consume a named build item without a name"); - } if (EmptyBuildItem.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Cannot consume an empty build item"); } - addConsumes(new ItemId(type, null), Constraint.REAL, ConsumeFlags.NONE); - return this; - } - - /** - * This build step consumes the given produced item. The item must be produced somewhere in the chain. If - * no such producer exists, the chain will not be constructed; instead, an error will be raised. - * - * @param type the item type (must not be {@code null}) - * @param name the build item name (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder consumes(Class> type, N name) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - addConsumes(new ItemId(type, name), Constraint.REAL, ConsumeFlags.NONE); + addConsumes(new ItemId(type), Constraint.REAL, ConsumeFlags.NONE); return this; } @@ -326,51 +178,10 @@ public BuildStepBuilder consumes(Class> type, N */ public BuildStepBuilder consumes(Class type, ConsumeFlags flags) { Assert.checkNotNullParam("type", type); - if (NamedBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot consume a named build item without a name"); - } if (EmptyBuildItem.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Cannot consume an empty build item"); } - addConsumes(new ItemId(type, null), Constraint.REAL, flags); - return this; - } - - /** - * This build step consumes the given produced item. The item must be produced somewhere in the chain. If - * no such producer exists, the chain will not be constructed; instead, an error will be raised. - * - * @param type the item type (must not be {@code null}) - * @param flags a set of flags which modify the consume operation (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder consumes(Class> type, N name, ConsumeFlags flags) { - Assert.checkNotNullParam("type", type); - Assert.checkNotNullParam("name", name); - addConsumes(new ItemId(type, name), Constraint.REAL, flags); - return this; - } - - /** - * Declare that the build step "consumes" a virtual item with the given identifier. - * - * @param symbolic the item identifier (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder afterVirtual(Enum symbolic) { - addConsumes(new ItemId(SymbolicBuildItem.class, symbolic), Constraint.REAL, ConsumeFlags.NONE); - return this; - } - - /** - * Declare that the build step "consumes" a virtual item with the given identifier. - * - * @param symbolic the item identifier (must not be {@code null}) - * @param flags a set of flags which modify the consume operation (must not be {@code null}) - * @return this builder - */ - public BuildStepBuilder afterVirtual(Enum symbolic, ConsumeFlags flags) { - addConsumes(new ItemId(SymbolicBuildItem.class, symbolic), Constraint.REAL, flags); + addConsumes(new ItemId(type), Constraint.REAL, flags); return this; } diff --git a/core/builder/src/main/java/io/quarkus/builder/ItemId.java b/core/builder/src/main/java/io/quarkus/builder/ItemId.java index d9a2bebb98565..e7f2708a2cb29 100644 --- a/core/builder/src/main/java/io/quarkus/builder/ItemId.java +++ b/core/builder/src/main/java/io/quarkus/builder/ItemId.java @@ -6,27 +6,19 @@ import io.quarkus.builder.item.BuildItem; import io.quarkus.builder.item.MultiBuildItem; -import io.quarkus.builder.item.NamedBuildItem; -import io.quarkus.builder.item.NamedMultiBuildItem; /** */ final class ItemId { private final Class itemType; - private final Object name; - ItemId(final Class itemType, final Object name) { + ItemId(final Class itemType) { Assert.checkNotNullParam("itemType", itemType); - if (NamedBuildItem.class.isAssignableFrom(itemType)) { - // todo: support default names - Assert.checkNotNullParam("name", name); - } this.itemType = itemType; - this.name = name; } boolean isMulti() { - return MultiBuildItem.class.isAssignableFrom(itemType) || NamedMultiBuildItem.class.isAssignableFrom(itemType); + return MultiBuildItem.class.isAssignableFrom(itemType); } @Override @@ -35,27 +27,17 @@ public boolean equals(Object obj) { } boolean equals(ItemId obj) { - return this == obj || obj != null && itemType == obj.itemType && Objects.equals(name, obj.name); + return this == obj || obj != null && itemType == obj.itemType; } @Override public int hashCode() { - return Objects.hashCode(name) * 31 + Objects.hashCode(itemType); + return Objects.hashCode(itemType); } @Override public String toString() { - final Object name = this.name; - final Class itemType = this.itemType; - if (name == null) { - assert itemType != null; - return itemType.toString(); - } else if (itemType == null) { - assert name != null; - return "name " + name.toString(); - } else { - return itemType.toString() + " with name " + name; - } + return itemType.toString(); } Class getType() { diff --git a/core/builder/src/main/java/io/quarkus/builder/item/NamedBuildItem.java b/core/builder/src/main/java/io/quarkus/builder/item/NamedBuildItem.java deleted file mode 100644 index b156358bb708b..0000000000000 --- a/core/builder/src/main/java/io/quarkus/builder/item/NamedBuildItem.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.quarkus.builder.item; - -/** - * A build item that can occur more than once in a build, discriminated by name. - * - * @param the name type - */ -@SuppressWarnings("unused") -public abstract class NamedBuildItem extends BuildItem { - NamedBuildItem() { - } -} diff --git a/core/builder/src/main/java/io/quarkus/builder/item/NamedMultiBuildItem.java b/core/builder/src/main/java/io/quarkus/builder/item/NamedMultiBuildItem.java deleted file mode 100644 index 512de226103ef..0000000000000 --- a/core/builder/src/main/java/io/quarkus/builder/item/NamedMultiBuildItem.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.quarkus.builder.item; - -/** - * A build item that can occur more than once in a build, discriminated by name. - * - * @param the name type - */ -@SuppressWarnings("unused") -public abstract class NamedMultiBuildItem extends BuildItem { - NamedMultiBuildItem() { - } -} diff --git a/core/builder/src/main/java/io/quarkus/builder/item/SymbolicBuildItem.java b/core/builder/src/main/java/io/quarkus/builder/item/SymbolicBuildItem.java deleted file mode 100644 index ac53fadc6dd91..0000000000000 --- a/core/builder/src/main/java/io/quarkus/builder/item/SymbolicBuildItem.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.quarkus.builder.item; - -/** - * The symbolic build item. - */ -public final class SymbolicBuildItem extends NamedBuildItem> { - - private static final SymbolicBuildItem INSTANCE = new SymbolicBuildItem(); - - private SymbolicBuildItem() { - } - - /** - * Get the singleton instance. - * - * @return the singleton instance (not {@code null}) - */ - public static SymbolicBuildItem getInstance() { - return INSTANCE; - } - - @Override - public int hashCode() { - return 0; - } - - @Override - public boolean equals(final Object obj) { - return obj == this; - } - - @Override - public String toString() { - return "symbolic"; - } -} diff --git a/core/creator/src/main/java/io/quarkus/creator/phase/augment/AugmentTask.java b/core/creator/src/main/java/io/quarkus/creator/phase/augment/AugmentTask.java index 2da772eaae17a..0ae7a3f8c65f0 100644 --- a/core/creator/src/main/java/io/quarkus/creator/phase/augment/AugmentTask.java +++ b/core/creator/src/main/java/io/quarkus/creator/phase/augment/AugmentTask.java @@ -15,7 +15,9 @@ import java.util.Properties; import java.util.function.Consumer; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigBuilder; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.logging.Logger; import io.quarkus.bootstrap.BootstrapDependencyProcessingException; @@ -36,8 +38,11 @@ import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.configuration.QuarkusConfigFactory; import io.smallrye.config.PropertiesConfigSource; -import io.smallrye.config.SmallRyeConfigProviderResolver; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; /** * This phase consumes {@link CurateOutcome} and processes @@ -110,32 +115,25 @@ public AugmentOutcome run(CurateOutcome appState, CuratedApplicationCreator ctx) } //first lets look for some config, as it is not on the current class path //and we need to load it to run the build process - Path config = configDir.resolve("application.properties"); - if (Files.exists(config)) { + Path configPath = configDir.resolve("application.properties"); + SmallRyeConfigBuilder configBuilder = ConfigUtils.configBuilder(false); + if (Files.exists(configPath)) { try { - ConfigBuilder builder = SmallRyeConfigProviderResolver.instance().getBuilder() - .addDefaultSources() - .addDiscoveredConverters() - .addDiscoveredSources() - .withSources(new PropertiesConfigSource(config.toUri().toURL())); - - if (configCustomizer != null) { - configCustomizer.accept(builder); - } - SmallRyeConfigProviderResolver.instance().registerConfig(builder.build(), - Thread.currentThread().getContextClassLoader()); - } catch (Exception e) { - throw new RuntimeException(e); + configBuilder.withSources(new PropertiesConfigSource(configPath.toUri().toURL())); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to convert config URL", e); } - } else if (configCustomizer != null) { - ConfigBuilder builder = SmallRyeConfigProviderResolver.instance().getBuilder() - .addDefaultSources() - .addDiscoveredConverters() - .addDiscoveredSources(); - - configCustomizer.accept(builder); - SmallRyeConfigProviderResolver.instance().registerConfig(builder.build(), - Thread.currentThread().getContextClassLoader()); + } + if (configCustomizer != null) { + configCustomizer.accept(configBuilder); + } + final SmallRyeConfig config = configBuilder.build(); + QuarkusConfigFactory.setConfig(config); + final ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + final Config existing = cpr.getConfig(); + if (existing != config) { + cpr.releaseConfig(existing); + // subsequent calls will get the new config } final AppModelResolver depResolver = appState.getArtifactResolver(); diff --git a/core/creator/src/main/java/io/quarkus/creator/phase/generateconfig/GenerateConfigTask.java b/core/creator/src/main/java/io/quarkus/creator/phase/generateconfig/GenerateConfigTask.java index 1532db5c95940..2b80246882d0d 100644 --- a/core/creator/src/main/java/io/quarkus/creator/phase/generateconfig/GenerateConfigTask.java +++ b/core/creator/src/main/java/io/quarkus/creator/phase/generateconfig/GenerateConfigTask.java @@ -16,6 +16,7 @@ import java.util.regex.Pattern; import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.logging.Logger; import io.quarkus.bootstrap.BootstrapDependencyProcessingException; @@ -31,7 +32,6 @@ import io.quarkus.creator.CuratedTask; import io.quarkus.creator.curator.CurateOutcome; import io.quarkus.deployment.ExtensionLoader; -import io.quarkus.deployment.QuarkusConfig; import io.quarkus.deployment.builditem.ArchiveRootBuildItem; import io.quarkus.deployment.builditem.ConfigDescriptionBuildItem; import io.quarkus.deployment.builditem.ExtensionClassLoaderBuildItem; @@ -40,8 +40,11 @@ import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.util.FileUtil; import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.configuration.QuarkusConfigFactory; import io.smallrye.config.PropertiesConfigSource; -import io.smallrye.config.SmallRyeConfigProviderResolver; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; /** * This phase generates an example configuration file @@ -65,12 +68,16 @@ public Path run(CurateOutcome appState, CuratedApplicationCreator creator) throw //TODO: do we actually need to load this config? Does it affect resolution? if (Files.exists(configFile)) { try { - Config built = SmallRyeConfigProviderResolver.instance().getBuilder() - .addDefaultSources() - .addDiscoveredConverters() - .addDiscoveredSources() - .withSources(new PropertiesConfigSource(configFile.toUri().toURL())).build(); - SmallRyeConfigProviderResolver.instance().registerConfig(built, Thread.currentThread().getContextClassLoader()); + SmallRyeConfigBuilder builder = ConfigUtils.configBuilder(false) + .withSources(new PropertiesConfigSource(configFile.toUri().toURL())); + final SmallRyeConfig config = builder.build(); + QuarkusConfigFactory.setConfig(config); + final ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + final Config existing = cpr.getConfig(); + if (existing != config) { + cpr.releaseConfig(existing); + // subsequent calls will get the new config + } } catch (Exception e) { throw new RuntimeException(e); } @@ -107,7 +114,6 @@ public Path run(CurateOutcome appState, CuratedApplicationCreator creator) throw chainBuilder.loadProviders(runnerClassLoader); chainBuilder - .addInitial(QuarkusConfig.class) .addInitial(ShutdownContextBuildItem.class) .addInitial(LaunchModeBuildItem.class) .addInitial(ArchiveRootBuildItem.class) @@ -118,7 +124,6 @@ public Path run(CurateOutcome appState, CuratedApplicationCreator creator) throw BuildChain chain = chainBuilder .build(); BuildExecutionBuilder execBuilder = chain.createExecutionBuilder("main") - .produce(QuarkusConfig.INSTANCE) .produce(new LaunchModeBuildItem(LaunchMode.NORMAL)) .produce(new ShutdownContextBuildItem()) .produce(new LiveReloadBuildItem()) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ApplicationConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/ApplicationConfig.java deleted file mode 100644 index 180ae5ad3a113..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/ApplicationConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.quarkus.deployment; - -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; - -@ConfigRoot(phase = ConfigPhase.BUILD_TIME) -public class ApplicationConfig { - - /** - * The name of the application. - * If not set, defaults to the name of the project. - */ - @ConfigItem - public String name; - - /** - * The version of the application. - * If not set, defaults to the version of the project - */ - @ConfigItem - public String version; -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capabilities.java b/core/deployment/src/main/java/io/quarkus/deployment/Capabilities.java index 1a3cd698e7bed..18aceabb84524 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capabilities.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capabilities.java @@ -24,7 +24,9 @@ public final class Capabilities extends SimpleBuildItem { public static final String SECURITY = "io.quarkus.security"; public static final String SECURITY_ELYTRON_OAUTH2 = "io.quarkus.elytron.security.oauth2"; public static final String SECURITY_ELYTRON_JDBC = "io.quarkus.elytron.security.jdbc"; + public static final String SECURITY_ELYTRON_LDAP = "io.quarkus.elytron.security.ldap"; public static final String QUARTZ = "io.quarkus.quartz"; + public static final String METRICS = "io.quarkus.metrics"; private final Set capabilities; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/DebugConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/DebugConfig.java new file mode 100644 index 0000000000000..e54d05c735570 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/DebugConfig.java @@ -0,0 +1,28 @@ +package io.quarkus.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * This is used currently only to suppress warnings about unknown properties + * when the user supplies something like: -Dquarkus.debug.reflection=true + * + * TODO refactor code to actually use these values + */ +@ConfigRoot +public class DebugConfig { + + /** + * If set to true, writes a list of all reflective classes to META-INF + */ + @ConfigItem(defaultValue = "false") + boolean reflection; + + /** + * If set to a directory, all generated classes will be written into that directory + */ + @ConfigItem + Optional generatedClassesDir; +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java index 2ccecac799b29..a1bb53b30753a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java @@ -25,12 +25,11 @@ import java.util.Collections; import java.util.EnumSet; 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.Properties; -import java.util.Set; import java.util.concurrent.Executor; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -40,6 +39,7 @@ import java.util.function.Supplier; import org.eclipse.microprofile.config.spi.ConfigBuilder; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.logging.Logger; import org.wildfly.common.function.Functions; @@ -62,33 +62,33 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.annotations.Weak; import io.quarkus.deployment.builditem.AdditionalApplicationArchiveMarkerBuildItem; -import io.quarkus.deployment.builditem.BuildTimeConfigurationBuildItem; -import io.quarkus.deployment.builditem.BuildTimeRunTimeFixedConfigurationBuildItem; +import io.quarkus.deployment.builditem.BytecodeRecorderObjectLoaderBuildItem; import io.quarkus.deployment.builditem.CapabilityBuildItem; +import io.quarkus.deployment.builditem.ConfigurationBuildItem; import io.quarkus.deployment.builditem.DeploymentClassLoaderBuildItem; import io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem; -import io.quarkus.deployment.builditem.RunTimeConfigurationBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigurationProxyBuildItem; import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem; -import io.quarkus.deployment.builditem.UnmatchedConfigBuildItem; -import io.quarkus.deployment.configuration.ConfigDefinition; +import io.quarkus.deployment.configuration.BuildTimeConfigurationReader; import io.quarkus.deployment.configuration.DefaultValuesConfigurationSource; +import io.quarkus.deployment.configuration.definition.RootDefinition; import io.quarkus.deployment.recording.BytecodeRecorderImpl; +import io.quarkus.deployment.recording.ObjectLoader; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.ReflectUtil; import io.quarkus.deployment.util.ServiceUtil; +import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.ResultHandle; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSource; -import io.quarkus.runtime.configuration.ConverterSupport; -import io.quarkus.runtime.configuration.DeploymentProfileConfigSource; -import io.quarkus.runtime.configuration.ExpandingConfigSource; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.configuration.QuarkusConfigFactory; import io.smallrye.config.PropertiesConfigSource; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; -import io.smallrye.config.SmallRyeConfigProviderResolver; /** * Utility class to load build steps, runtime recorders, and configuration roots from a given extension class. @@ -98,17 +98,6 @@ private ExtensionLoader() { } private static final Logger cfgLog = Logger.getLogger("io.quarkus.configuration"); - - public static final String BUILD_TIME_CONFIG = "io.quarkus.runtime.generated.BuildTimeConfig"; - public static final String BUILD_TIME_CONFIG_ROOT = "io.quarkus.runtime.generated.BuildTimeConfigRoot"; - public static final String RUN_TIME_CONFIG = "io.quarkus.runtime.generated.RunTimeConfig"; - public static final String RUN_TIME_CONFIG_ROOT = "io.quarkus.runtime.generated.RunTimeConfigRoot"; - - private static final FieldDescriptor RUN_TIME_CONFIG_FIELD = FieldDescriptor.of(RUN_TIME_CONFIG, "runConfig", - RUN_TIME_CONFIG_ROOT); - private static final FieldDescriptor BUILD_TIME_CONFIG_FIELD = FieldDescriptor.of(BUILD_TIME_CONFIG, "buildConfig", - BUILD_TIME_CONFIG_ROOT); - private static final String CONFIG_ROOTS_LIST = "META-INF/quarkus-config-roots.list"; @SuppressWarnings("deprecation") @@ -171,88 +160,80 @@ public static Consumer loadStepsFrom(ClassLoader classLoader, LaunchMode launchMode, Consumer configCustomizer) throws IOException, ClassNotFoundException { - // set up the configuration definitions - final ConfigDefinition buildTimeConfig = new ConfigDefinition(FieldDescriptor.of("Bogus", "No field", "Nothing")); - final ConfigDefinition buildTimeRunTimeConfig = new ConfigDefinition(BUILD_TIME_CONFIG_FIELD); - final ConfigDefinition runTimeConfig = new ConfigDefinition(RUN_TIME_CONFIG_FIELD, true); - - // populate it with all known types + // populate with all known types + List> roots = new ArrayList<>(); for (Class clazz : ServiceUtil.classesNamedIn(classLoader, CONFIG_ROOTS_LIST)) { final ConfigRoot annotation = clazz.getAnnotation(ConfigRoot.class); if (annotation == null) { cfgLog.warnf("Ignoring configuration root %s because it has no annotation", clazz); } else { - final ConfigPhase phase = annotation.phase(); - if (phase == ConfigPhase.RUN_TIME) { - runTimeConfig.registerConfigRoot(clazz); - } else if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { - buildTimeRunTimeConfig.registerConfigRoot(clazz); - } else if (phase == ConfigPhase.BUILD_TIME) { - buildTimeConfig.registerConfigRoot(clazz); - } else { - cfgLog.warnf("Unrecognized configuration phase \"%s\" on %s", phase, clazz); - } + roots.add(clazz); } } + final BuildTimeConfigurationReader reader = new BuildTimeConfigurationReader(roots); + // now prepare & load the build configuration - final SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder(); - - // expand properties - final ExpandingConfigSource.Cache cache = new ExpandingConfigSource.Cache(); - builder.withWrapper(ExpandingConfigSource.wrapper(cache)); - builder.withWrapper(DeploymentProfileConfigSource.wrapper()); - builder.addDefaultSources(); - final ApplicationPropertiesConfigSource.InJar inJar = new ApplicationPropertiesConfigSource.InJar(); - final DefaultValuesConfigurationSource defaultSource = new DefaultValuesConfigurationSource( - buildTimeConfig.getLeafPatterns()); - final PropertiesConfigSource pcs = new PropertiesConfigSource(buildSystemProps, "Build system"); + final SmallRyeConfigBuilder builder = ConfigUtils.configBuilder(false); - builder.withSources(inJar, defaultSource, pcs); + final DefaultValuesConfigurationSource ds1 = new DefaultValuesConfigurationSource( + reader.getBuildTimePatternMap()); + final DefaultValuesConfigurationSource ds2 = new DefaultValuesConfigurationSource( + reader.getBuildTimeRunTimePatternMap()); + final PropertiesConfigSource pcs = new PropertiesConfigSource(buildSystemProps, "Build system"); - // populate builder with all converters loaded from ServiceLoader - ConverterSupport.populateConverters(builder); + builder.withSources(ds1, ds2, pcs); if (configCustomizer != null) { configCustomizer.accept(builder); } - final SmallRyeConfig src = (SmallRyeConfig) builder - .addDefaultSources() - .addDiscoveredSources() - .addDiscoveredConverters() - .build(); - - SmallRyeConfigProviderResolver.instance().registerConfig(src, classLoader); - - Set unmatched = new HashSet<>(); - - ConfigDefinition.loadConfiguration(cache, src, - unmatched, - buildTimeConfig, - buildTimeRunTimeConfig, // this one is only for generating a default-values config source - runTimeConfig); + final SmallRyeConfig src = builder.build(); + + // install globally + QuarkusConfigFactory.setConfig(src); + final ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + try { + cpr.releaseConfig(cpr.getConfig()); + } catch (IllegalStateException ignored) { + // just means no config was installed, which is fine + } - unmatched.removeIf(s -> !inJar.getPropertyNames().contains(s) && !s.startsWith("quarkus.")); + final BuildTimeConfigurationReader.ReadResult readResult = reader.readConfiguration(src); + // the proxy objects used for run time config in the recorders + Map, Object> proxies = new HashMap<>(); Consumer result = Functions.discardingConsumer(); - result = result.andThen(bcb -> bcb.addBuildStep(bc -> { - bc.produce(new BuildTimeConfigurationBuildItem(buildTimeConfig)); - bc.produce(new BuildTimeRunTimeFixedConfigurationBuildItem(buildTimeRunTimeConfig)); - bc.produce(new RunTimeConfigurationBuildItem(runTimeConfig)); - bc.produce(new UnmatchedConfigBuildItem(Collections.unmodifiableSet(unmatched))); - }).produces(BuildTimeConfigurationBuildItem.class) - .produces(BuildTimeRunTimeFixedConfigurationBuildItem.class) - .produces(RunTimeConfigurationBuildItem.class) - .produces(UnmatchedConfigBuildItem.class) - .build()); for (Class clazz : ServiceUtil.classesNamedIn(classLoader, "META-INF/quarkus-build-steps.list")) { try { - result = result - .andThen(ExtensionLoader.loadStepsFrom(clazz, buildTimeConfig, buildTimeRunTimeConfig, launchMode)); + result = result.andThen( + ExtensionLoader.loadStepsFrom(clazz, readResult, proxies, launchMode)); } catch (Throwable e) { throw new RuntimeException("Failed to load steps from " + clazz, e); } } + // this has to be an identity hash map else the recorder will get angry + Map proxyFields = new IdentityHashMap<>(); + for (Map.Entry, Object> entry : proxies.entrySet()) { + final RootDefinition def = readResult.requireRootDefinitionForClass(entry.getKey()); + proxyFields.put(entry.getValue(), def.getDescriptor()); + } + result = result.andThen(bcb -> bcb.addBuildStep(bc -> { + bc.produce(new ConfigurationBuildItem(readResult)); + bc.produce(new RunTimeConfigurationProxyBuildItem(proxies)); + final ObjectLoader loader = new ObjectLoader() { + public ResultHandle load(final BytecodeCreator body, final Object obj, final boolean staticInit) { + return body.readStaticField(proxyFields.get(obj)); + } + + public boolean canHandleObject(final Object obj, final boolean staticInit) { + return proxyFields.containsKey(obj); + } + }; + bc.produce(new BytecodeRecorderObjectLoaderBuildItem(loader)); + }).produces(ConfigurationBuildItem.class) + .produces(RunTimeConfigurationProxyBuildItem.class) + .produces(BytecodeRecorderObjectLoaderBuildItem.class) + .build()); return result; } @@ -260,13 +241,13 @@ public static Consumer loadStepsFrom(ClassLoader classLoader, * Load all the build steps from the given class. * * @param clazz the class to load from (must not be {@code null}) - * @param buildTimeConfig the build time configuration (must not be {@code null}) - * @param buildTimeRunTimeConfig the build time/run time visible config (must not be {@code null}) - * @param launchMode + * @param readResult the build time configuration read result (must not be {@code null}) + * @param runTimeProxies the map of run time proxy objects to populate for recorders (must not be {@code null}) + * @param launchMode the launch mode * @return a consumer which adds the steps to the given chain builder */ - public static Consumer loadStepsFrom(Class clazz, ConfigDefinition buildTimeConfig, - ConfigDefinition buildTimeRunTimeConfig, final LaunchMode launchMode) { + public static Consumer loadStepsFrom(Class clazz, BuildTimeConfigurationReader.ReadResult readResult, + Map, Object> runTimeProxies, final LaunchMode launchMode) { final Constructor[] constructors = clazz.getDeclaredConstructors(); // this is the chain configuration that will contain all steps on this class and be returned Consumer chainConfig = Functions.discardingConsumer(); @@ -365,15 +346,14 @@ public static Consumer loadStepsFrom(Class clazz, ConfigDe final ConfigPhase phase = annotation.phase(); consumingConfigPhases.add(phase); - if (phase == ConfigPhase.BUILD_TIME) { - ctorParamFns.add(bc -> bc.consume(BuildTimeConfigurationBuildItem.class).getConfigDefinition() - .getRealizedInstance(parameterClass)); - } else if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { - ctorParamFns.add(bc -> bc.consume(BuildTimeRunTimeFixedConfigurationBuildItem.class) - .getConfigDefinition().getRealizedInstance(parameterClass)); + if (phase.isAvailableAtBuild()) { + ctorParamFns.add(bc -> bc.consume(ConfigurationBuildItem.class).getReadResult() + .requireRootObjectForClass(parameterClass)); + if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { + runTimeProxies.computeIfAbsent(parameterClass, readResult::requireRootObjectForClass); + } } else if (phase == ConfigPhase.RUN_TIME) { - ctorParamFns.add(bc -> bc.consume(RunTimeConfigurationBuildItem.class).getConfigDefinition() - .getRealizedInstance(parameterClass)); + throw reportError(parameter, "Run time configuration cannot be consumed here"); } else { throw reportError(parameterClass, "Unknown value for ConfigPhase"); } @@ -477,27 +457,18 @@ public static Consumer loadStepsFrom(Class clazz, ConfigDe final ConfigPhase phase = annotation.phase(); consumingConfigPhases.add(phase); - if (phase == ConfigPhase.BUILD_TIME) { - stepInstanceSetup = stepInstanceSetup.andThen((bc, o) -> { - final BuildTimeConfigurationBuildItem configurationBuildItem = bc - .consume(BuildTimeConfigurationBuildItem.class); - ReflectUtil.setFieldVal(field, o, - configurationBuildItem.getConfigDefinition().getRealizedInstance(fieldClass)); - }); - } else if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { + if (phase.isAvailableAtBuild()) { stepInstanceSetup = stepInstanceSetup.andThen((bc, o) -> { - final BuildTimeRunTimeFixedConfigurationBuildItem configurationBuildItem = bc - .consume(BuildTimeRunTimeFixedConfigurationBuildItem.class); + final ConfigurationBuildItem configurationBuildItem = bc + .consume(ConfigurationBuildItem.class); ReflectUtil.setFieldVal(field, o, - configurationBuildItem.getConfigDefinition().getRealizedInstance(fieldClass)); + configurationBuildItem.getReadResult().requireRootObjectForClass(fieldClass)); }); + if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { + runTimeProxies.computeIfAbsent(fieldClass, readResult::requireRootObjectForClass); + } } else if (phase == ConfigPhase.RUN_TIME) { - stepInstanceSetup = stepInstanceSetup.andThen((bc, o) -> { - final RunTimeConfigurationBuildItem configurationBuildItem = bc - .consume(RunTimeConfigurationBuildItem.class); - ReflectUtil.setFieldVal(field, o, - configurationBuildItem.getConfigDefinition().getRealizedInstance(fieldClass)); - }); + throw reportError(field, "Run time configuration cannot be consumed here"); } else { throw reportError(fieldClass, "Unknown value for ConfigPhase"); } @@ -566,16 +537,14 @@ public static Consumer loadStepsFrom(Class clazz, ConfigDe } else if (parameterClass.isAnnotationPresent(ConfigRoot.class)) { final ConfigRoot annotation = parameterClass.getAnnotation(ConfigRoot.class); final ConfigPhase phase = annotation.phase(); - ConfigDefinition confDef; - if (phase == ConfigPhase.BUILD_TIME) { - confDef = buildTimeConfig; - } else if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { - confDef = buildTimeRunTimeConfig; + if (phase.isAvailableAtBuild()) { + paramSuppList.add(() -> readResult.requireRootObjectForClass(parameterClass)); + } else if (phase == ConfigPhase.RUN_TIME) { + throw reportError(parameter, "Run time configuration cannot be consumed here"); } else { throw reportError(parameter, "Unsupported conditional class configuration build phase " + phase); } - paramSuppList.add(() -> confDef.getRealizedInstance(parameterClass)); } else { throw reportError(parameter, "Unsupported conditional class constructor parameter type " + parameterClass); @@ -600,17 +569,15 @@ public static Consumer loadStepsFrom(Class clazz, ConfigDe } else if (fieldClass.isAnnotationPresent(ConfigRoot.class)) { final ConfigRoot annotation = fieldClass.getAnnotation(ConfigRoot.class); final ConfigPhase phase = annotation.phase(); - ConfigDefinition confDef; - if (phase == ConfigPhase.BUILD_TIME) { - confDef = buildTimeConfig; - } else if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { - confDef = buildTimeRunTimeConfig; + if (phase.isAvailableAtBuild()) { + setup = setup.andThen(o -> ReflectUtil.setFieldVal(field, o, + readResult.requireRootObjectForClass(fieldClass))); + } else if (phase == ConfigPhase.RUN_TIME) { + throw reportError(field, "Run time configuration cannot be consumed here"); } else { throw reportError(field, "Unsupported conditional class configuration build phase " + phase); } - setup = setup.andThen( - o -> ReflectUtil.setFieldVal(field, o, confDef.getRealizedInstance(fieldClass))); } else { throw reportError(field, "Unsupported conditional class field type " + fieldClass); } @@ -757,24 +724,27 @@ public static Consumer loadStepsFrom(Class clazz, ConfigDe final ConfigPhase phase = annotation.phase(); methodConsumingConfigPhases.add(phase); - if (phase == ConfigPhase.BUILD_TIME) { + if (phase.isAvailableAtBuild()) { methodParamFns.add((bc, bri) -> { - final BuildTimeConfigurationBuildItem configurationBuildItem = bc - .consume(BuildTimeConfigurationBuildItem.class); - return configurationBuildItem.getConfigDefinition().getRealizedInstance(parameterClass); - }); - } else if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { - methodParamFns.add((bc, bri) -> { - final BuildTimeRunTimeFixedConfigurationBuildItem configurationBuildItem = bc - .consume(BuildTimeRunTimeFixedConfigurationBuildItem.class); - return configurationBuildItem.getConfigDefinition().getRealizedInstance(parameterClass); + final ConfigurationBuildItem configurationBuildItem = bc + .consume(ConfigurationBuildItem.class); + return configurationBuildItem.getReadResult().requireRootObjectForClass(parameterClass); }); + if (isRecorder && phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { + runTimeProxies.computeIfAbsent(parameterClass, readResult::requireRootObjectForClass); + } } else if (phase == ConfigPhase.RUN_TIME) { - methodParamFns.add((bc, bri) -> { - final RunTimeConfigurationBuildItem configurationBuildItem = bc - .consume(RunTimeConfigurationBuildItem.class); - return configurationBuildItem.getConfigDefinition().getRealizedInstance(parameterClass); - }); + if (isRecorder) { + methodParamFns.add((bc, bri) -> { + final RunTimeConfigurationProxyBuildItem proxies = bc + .consume(RunTimeConfigurationProxyBuildItem.class); + return proxies.getProxyObjectFor(parameterClass); + }); + runTimeProxies.computeIfAbsent(parameterClass, ReflectUtil::newInstance); + } else { + throw reportError(parameter, + "Run time configuration cannot be consumed here unless the method is a @Recorder"); + } } else { throw reportError(parameterClass, "Unknown value for ConfigPhase"); } @@ -873,15 +843,12 @@ public static Consumer loadStepsFrom(Class clazz, ConfigDe "Bytecode recorder is static but an injected config object is declared as run time"); } methodStepConfig = methodStepConfig - .andThen(bsb -> bsb.consumes(RunTimeConfigurationBuildItem.class)); - } - if (methodConsumingConfigPhases.contains(ConfigPhase.BUILD_AND_RUN_TIME_FIXED)) { - methodStepConfig = methodStepConfig - .andThen(bsb -> bsb.consumes(BuildTimeRunTimeFixedConfigurationBuildItem.class)); + .andThen(bsb -> bsb.consumes(RunTimeConfigurationProxyBuildItem.class)); } - if (methodConsumingConfigPhases.contains(ConfigPhase.BUILD_TIME)) { + if (methodConsumingConfigPhases.contains(ConfigPhase.BUILD_AND_RUN_TIME_FIXED) + || methodConsumingConfigPhases.contains(ConfigPhase.BUILD_TIME)) { methodStepConfig = methodStepConfig - .andThen(bsb -> bsb.consumes(BuildTimeConfigurationBuildItem.class)); + .andThen(bsb -> bsb.consumes(ConfigurationBuildItem.class)); } final Consume[] consumes = method.getAnnotationsByType(Consume.class); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java index ed314d1ae7649..1b667e9618af1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/JniProcessor.java @@ -1,6 +1,8 @@ package io.quarkus.deployment; +import java.util.Collections; import java.util.List; +import java.util.Optional; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -19,7 +21,7 @@ static class JniConfig { * Paths of library to load. */ @ConfigItem - List libraryPaths; + Optional> libraryPaths; /** * Enable JNI support. @@ -30,8 +32,8 @@ static class JniConfig { @BuildStep void setupJni(BuildProducer jniProducer) { - if ((jni.enable) || !jni.libraryPaths.isEmpty()) { - jniProducer.produce(new JniBuildItem(jni.libraryPaths)); + if ((jni.enable) || jni.libraryPaths.isPresent()) { + jniProducer.produce(new JniBuildItem(jni.libraryPaths.orElse(Collections.emptyList()))); } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/LiveReloadConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/LiveReloadConfig.java new file mode 100644 index 0000000000000..b754b3355ba63 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/LiveReloadConfig.java @@ -0,0 +1,28 @@ +package io.quarkus.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * This is used currently only to suppress warnings about unknown properties + * when the user supplies something like: -Dquarkus.live-reload.password=secret + * + * TODO refactor code to actually use these values + */ +@ConfigRoot +public class LiveReloadConfig { + + /** + * Password used to use to connect to the remote dev-mode application + */ + @ConfigItem + Optional password; + + /** + * URL used to use to connect to the remote dev-mode application + */ + @ConfigItem + Optional url; +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/PlatformConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/PlatformConfig.java new file mode 100644 index 0000000000000..bfef4ee947b7a --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/PlatformConfig.java @@ -0,0 +1,32 @@ +package io.quarkus.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * This is used currently only to suppress warnings about unknown properties + * when the user supplies something like: -Dquarkus.platform.group-id=someGroup + * + * TODO refactor code to actually use these values + */ +@ConfigRoot +public class PlatformConfig { + + /** + * groupId of the platform to use + */ + @ConfigItem(defaultValue = "io.quarkus") + String groupId; + + /** + * artifactId of the platform to use + */ + @ConfigItem(defaultValue = "quarkus-universe-bom") + String artifactId; + + /** + * version of the platform to use + */ + @ConfigItem(defaultValue = "999-SNAPSHOT") + String version; +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java index c4e9f2990bcee..83af5936d600c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java @@ -31,8 +31,8 @@ import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.runtime.LaunchMode; public class QuarkusAugmentor { @@ -90,14 +90,13 @@ public BuildResult run() throws Exception { chainBuilder.loadProviders(classLoader); chainBuilder - .addInitial(QuarkusConfig.class) .addInitial(ArchiveRootBuildItem.class) .addInitial(ShutdownContextBuildItem.class) .addInitial(LaunchModeBuildItem.class) .addInitial(LiveReloadBuildItem.class) .addInitial(AdditionalApplicationArchiveBuildItem.class) .addInitial(ExtensionClassLoaderBuildItem.class) - .addInitial(OutputTargetBuildItem.class) + .addInitial(BuildSystemTargetBuildItem.class) .addInitial(CurateOutcomeBuildItem.class); for (Class i : finalResults) { chainBuilder.addFinal(i); @@ -115,13 +114,12 @@ public BuildResult run() throws Exception { rootFs = FileSystems.newFileSystem(root, null); } BuildExecutionBuilder execBuilder = chain.createExecutionBuilder("main") - .produce(QuarkusConfig.INSTANCE) .produce(liveReloadBuildItem) .produce(new ArchiveRootBuildItem(root, rootFs == null ? root : rootFs.getPath("/"), excludedFromIndexing)) .produce(new ShutdownContextBuildItem()) .produce(new LaunchModeBuildItem(launchMode)) .produce(new ExtensionClassLoaderBuildItem(classLoader)) - .produce(new OutputTargetBuildItem(targetDir, baseName)) + .produce(new BuildSystemTargetBuildItem(targetDir, baseName)) .produce(new CurateOutcomeBuildItem(effectiveModel, resolver)); for (Path i : additionalApplicationArchives) { execBuilder.produce(new AdditionalApplicationArchiveBuildItem(i)); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusClassWriter.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusClassWriter.java index 1b32a46a504a0..f65a543c9dac1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusClassWriter.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusClassWriter.java @@ -20,21 +20,8 @@ public QuarkusClassWriter(final int flags) { } @Override - protected String getCommonSuperClass(String type1, String type2) { - ClassLoader cl = getClassLoader(); - Class c1 = null, c2 = null; - try { - c1 = cl.loadClass(type1.replace('/', '.')); - } catch (ClassNotFoundException e) { - } - try { - c2 = cl.loadClass(type2.replace('/', '.')); - } catch (ClassNotFoundException e) { - } - if (c1 != null && c2 != null) { - return super.getCommonSuperClass(type1, type2); - } - return Object.class.getName().replace('.', '/'); + protected ClassLoader getClassLoader() { + // the TCCL is safe for transformations when this ClassWriter runs + return Thread.currentThread().getContextClassLoader(); } - } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusConfig.java index 96d949aa42914..fee7c7c85cbb7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusConfig.java @@ -12,6 +12,10 @@ import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.configuration.ConfigurationError; +/** + * @deprecated Do not use this class anymore, instead try {@code ConfigProvider.getConfig.getValue()} instead. + */ +@Deprecated public final class QuarkusConfig extends SimpleBuildItem { public static final QuarkusConfig INSTANCE = new QuarkusConfig(); @@ -42,11 +46,11 @@ public static Set getNames(String prefix) { Set props = new HashSet<>(); for (String i : ConfigProvider.getConfig().getPropertyNames()) { if (i.startsWith(prefix)) { - int idex = i.indexOf('.', prefix.length() + 1); - if (idex == -1) { + int index = i.indexOf('.', prefix.length() + 1); + if (index == -1) { props.add(i.substring(prefix.length() + 1)); } else { - props.add(i.substring(prefix.length() + 1, idex)); + props.add(i.substring(prefix.length() + 1, index)); } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java new file mode 100644 index 0000000000000..4f50ec7a479e5 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java @@ -0,0 +1,28 @@ +package io.quarkus.deployment; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * This is used currently only to suppress warnings about unknown properties + * when the user supplies something like: -Dquarkus.test.native-image-profile=someProfile + * + * TODO refactor code to actually use these values + */ +@ConfigRoot +public class TestConfig { + + /** + * Duration to wait for the native image to built during testing + */ + @ConfigItem(defaultValue = "PT5M") + Duration nativeImageWaitTime; + + /** + * The profile to use when testing the native image + */ + @ConfigItem(defaultValue = "prod") + String nativeImageProfile; +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ThreadLocalRandomProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/ThreadLocalRandomProcessor.java deleted file mode 100644 index 6bbd0c0f55cee..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/ThreadLocalRandomProcessor.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.quarkus.deployment; - -import java.util.concurrent.ThreadLocalRandom; - -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; - -public class ThreadLocalRandomProcessor { - @BuildStep - RuntimeReinitializedClassBuildItem registerThreadLocalRandomReinitialize() { - // ThreadLocalRandom is bugged currently and doesn't reset the seeder - // See https://github.com/oracle/graal/issues/1614 for more details - return new RuntimeReinitializedClassBuildItem(ThreadLocalRandom.class.getName()); - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationInfoBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationInfoBuildItem.java index b2f3e696c276c..46502b45e8d41 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationInfoBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ApplicationInfoBuildItem.java @@ -1,5 +1,7 @@ package io.quarkus.deployment.builditem; +import java.util.Optional; + import io.quarkus.builder.item.SimpleBuildItem; public final class ApplicationInfoBuildItem extends SimpleBuildItem { @@ -9,16 +11,16 @@ public final class ApplicationInfoBuildItem extends SimpleBuildItem { private final String name; private final String version; - public ApplicationInfoBuildItem(String name, String version) { - this.name = name; - this.version = version; + public ApplicationInfoBuildItem(Optional name, Optional version) { + this.name = name.orElse(UNSET_VALUE); + this.version = version.orElse(UNSET_VALUE); } public String getName() { - return name == null ? UNSET_VALUE : name; + return name; } public String getVersion() { - return version == null ? UNSET_VALUE : version; + return version; } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java index 35a241d2cb8e7..0801469938b0c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java @@ -31,7 +31,7 @@ public ArchiveRootBuildItem(Path archiveLocation, Path archiveRoot, Collection

> formatterValue; + + /** + * Construct a new instance. + * + * @param formatterValue the optional formatter run time value to use (must not be {@code null}) + */ + public LogConsoleFormatBuildItem(final RuntimeValue> formatterValue) { + this.formatterValue = Assert.checkNotNullParam("formatterValue", formatterValue); + } + + /** + * Get the formatter value. + * + * @return the formatter value + */ + public RuntimeValue> getFormatterValue() { + return formatterValue; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogHandlerBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogHandlerBuildItem.java new file mode 100644 index 0000000000000..963c17912dcc8 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogHandlerBuildItem.java @@ -0,0 +1,34 @@ +package io.quarkus.deployment.builditem; + +import java.util.Optional; +import java.util.logging.Handler; + +import org.wildfly.common.Assert; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * A build item for adding additional logging handlers. + */ +public final class LogHandlerBuildItem extends MultiBuildItem { + private final RuntimeValue> handlerValue; + + /** + * Construct a new instance. + * + * @param handlerValue the handler value to add to the run time configuration + */ + public LogHandlerBuildItem(final RuntimeValue> handlerValue) { + this.handlerValue = Assert.checkNotNullParam("handlerValue", handlerValue); + } + + /** + * Get the handler value. + * + * @return the handler value + */ + public RuntimeValue> getHandlerValue() { + return handlerValue; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeEnableAllCharsetsBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeEnableAllCharsetsBuildItem.java index 2e2426cf91adf..e04266e7b0818 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeEnableAllCharsetsBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeEnableAllCharsetsBuildItem.java @@ -2,6 +2,10 @@ import io.quarkus.builder.item.MultiBuildItem; +/** + * @deprecated use {@link NativeImageEnableAllCharsetsBuildItem} instead + */ +@Deprecated public final class NativeEnableAllCharsetsBuildItem extends MultiBuildItem { } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeImageEnableAllCharsetsBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeImageEnableAllCharsetsBuildItem.java new file mode 100644 index 0000000000000..36e6e50cd5a08 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeImageEnableAllCharsetsBuildItem.java @@ -0,0 +1,7 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class NativeImageEnableAllCharsetsBuildItem extends MultiBuildItem { + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeImageEnableAllTimeZonesBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeImageEnableAllTimeZonesBuildItem.java new file mode 100644 index 0000000000000..4223eceba4d21 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeImageEnableAllTimeZonesBuildItem.java @@ -0,0 +1,7 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class NativeImageEnableAllTimeZonesBuildItem extends MultiBuildItem { + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/RunTimeConfigurationBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/RunTimeConfigurationBuildItem.java deleted file mode 100644 index 83fe0e8ad0fc1..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/RunTimeConfigurationBuildItem.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkus.deployment.builditem; - -import io.quarkus.builder.item.SimpleBuildItem; -import io.quarkus.deployment.configuration.ConfigDefinition; - -/** - * The build item which carries the run time configuration. - */ -public final class RunTimeConfigurationBuildItem extends SimpleBuildItem { - private final ConfigDefinition configDefinition; - - public RunTimeConfigurationBuildItem(final ConfigDefinition configDefinition) { - this.configDefinition = configDefinition; - } - - public ConfigDefinition getConfigDefinition() { - return configDefinition; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/RunTimeConfigurationProxyBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/RunTimeConfigurationProxyBuildItem.java new file mode 100644 index 0000000000000..7fb9c3a7e12bb --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/RunTimeConfigurationProxyBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.deployment.builditem; + +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * A build item that carries all the "fake" run time config objects for use by recorders. + */ +public final class RunTimeConfigurationProxyBuildItem extends SimpleBuildItem { + private final Map, Object> objects; + + public RunTimeConfigurationProxyBuildItem(final Map, Object> objects) { + this.objects = objects; + } + + public Object getProxyObjectFor(Class clazz) { + return objects.get(clazz); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/UnmatchedConfigBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/UnmatchedConfigBuildItem.java deleted file mode 100644 index efa0d13064d48..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/UnmatchedConfigBuildItem.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.quarkus.deployment.builditem; - -import java.util.Set; - -import org.wildfly.common.Assert; - -import io.quarkus.builder.item.SimpleBuildItem; - -/** - * An internal build item which relays the unmatched configuration key set from the extension loader - * to configuration setup stages. - */ -public final class UnmatchedConfigBuildItem extends SimpleBuildItem { - private final Set set; - - /** - * Construct a new instance. - * - * @param set the non-{@code null}, immutable set - */ - public UnmatchedConfigBuildItem(final Set set) { - Assert.checkNotNullParam("set", set); - this.set = set; - } - - /** - * Get the set. - * - * @return the set - */ - public Set getSet() { - return set; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceDirectoryBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceDirectoryBuildItem.java new file mode 100644 index 0000000000000..60847c404ac56 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceDirectoryBuildItem.java @@ -0,0 +1,19 @@ +package io.quarkus.deployment.builditem.nativeimage; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * A build item that indicates that directory resources should be included in the native image + */ +public final class NativeImageResourceDirectoryBuildItem extends MultiBuildItem { + + private final String path; + + public NativeImageResourceDirectoryBuildItem(String path) { + this.path = path; + } + + public String getPath() { + return path; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveClassFinalFieldsWritablePredicateBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveClassFinalFieldsWritablePredicateBuildItem.java new file mode 100644 index 0000000000000..48140a71f201f --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveClassFinalFieldsWritablePredicateBuildItem.java @@ -0,0 +1,26 @@ +package io.quarkus.deployment.builditem.nativeimage; + +import java.util.function.Predicate; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Used by {@code io.quarkus.deployment.steps.ReflectiveHierarchyStep} to determine whether or + * not the final fields of the class should be writable (which they aren't by default) + * + * If any one of the predicates returns true for a class, then ReflectiveHierarchyStep uses that true value + */ +public final class ReflectiveClassFinalFieldsWritablePredicateBuildItem extends MultiBuildItem { + + private final Predicate predicate; + + public ReflectiveClassFinalFieldsWritablePredicateBuildItem(Predicate predicate) { + this.predicate = predicate; + } + + public Predicate getPredicate() { + return predicate; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/substrate/DeprecatedBuildItemProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/substrate/DeprecatedBuildItemProcessor.java index 2fe553f189d55..b6576c76545a3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/substrate/DeprecatedBuildItemProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/substrate/DeprecatedBuildItemProcessor.java @@ -5,6 +5,8 @@ import java.util.Map; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.NativeEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; @@ -192,4 +194,9 @@ List substrateSystemProperties(List oldProps) { + return new NativeImageEnableAllCharsetsBuildItem(); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BooleanConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BooleanConfigType.java deleted file mode 100644 index ec86a06eeeeff..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BooleanConfigType.java +++ /dev/null @@ -1,133 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; -import java.util.Optional; - -import org.eclipse.microprofile.config.spi.Converter; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.deployment.steps.ConfigurationSetup; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class BooleanConfigType extends LeafConfigType { - private static final MethodDescriptor BOOL_VALUE_METHOD = MethodDescriptor.ofMethod(Boolean.class, "booleanValue", - boolean.class); - - final String defaultValue; - private final Class> converterClass; - - public BooleanConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final String defaultValue, String javadocKey, String configKey, - Class> converterClass) { - super(containingName, container, consumeSegment, javadocKey, configKey); - this.defaultValue = defaultValue; - this.converterClass = converterClass; - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - name.previous(); - container.acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - public void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - Optional optionalValue = ConfigUtils.getOptionalValue(config, name.toString(), Boolean.class, - converterClass); - field.setBoolean(enclosing, optionalValue.orElse(Boolean.FALSE).booleanValue()); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - public void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - // ConfigUtils.getOptionalValue(config, name.toString(), Boolean.class, converterClass).orElse(Boolean.FALSE).booleanValue() - final ResultHandle optionalValue = body.checkCast(body.invokeStaticMethod( - CU_GET_OPT_VALUE, - config, - body.invokeVirtualMethod( - OBJ_TO_STRING_METHOD, - name), - body.loadClass(Boolean.class), loadConverterClass(body)), Optional.class); - final ResultHandle convertedDefault = body.readStaticField(FieldDescriptor.of(Boolean.class, "FALSE", Boolean.class)); - final ResultHandle defaultedValue = body.checkCast(body.invokeVirtualMethod( - OPT_OR_ELSE_METHOD, - optionalValue, - convertedDefault), Boolean.class); - final ResultHandle booleanValue = body.invokeVirtualMethod(BOOL_VALUE_METHOD, defaultedValue); - body.invokeStaticMethod(setter, enclosing, booleanValue); - } - - public String getDefaultValueString() { - return defaultValue; - } - - @Override - public Class> getConverterClass() { - return converterClass; - } - - @Override - public Class getItemClass() { - return boolean.class; - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - Boolean value = ConfigUtils.convert(config, ExpandingConfigSource.expandValue(defaultValue, cache), Boolean.class, - converterClass); - field.setBoolean(enclosing, value.booleanValue()); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - final ResultHandle value = body.invokeVirtualMethod(BOOL_VALUE_METHOD, getConvertedDefault(body, cache, config)); - body.invokeStaticMethod(setter, enclosing, value); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - return body.invokeVirtualMethod(BOOL_VALUE_METHOD, getConvertedDefault(body, cache, smallRyeConfig)); - } - - private ResultHandle getConvertedDefault(final BytecodeCreator body, final ResultHandle cache, final ResultHandle config) { - return body.invokeStaticMethod( - CU_CONVERT, - config, - cache == null ? body.load(defaultValue) - : body.invokeStaticMethod( - ConfigurationSetup.ECS_EXPAND_VALUE, - body.load(defaultValue), - cache), - body.loadClass(Boolean.class), loadConverterClass(body)); - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java new file mode 100644 index 0000000000000..44e6153910e6c --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -0,0 +1,759 @@ +package io.quarkus.deployment.configuration; + +import static io.quarkus.deployment.util.ReflectUtil.rawTypeOf; +import static io.quarkus.deployment.util.ReflectUtil.rawTypeOfParameter; +import static io.quarkus.deployment.util.ReflectUtil.reportError; +import static io.quarkus.deployment.util.ReflectUtil.toError; +import static io.quarkus.deployment.util.ReflectUtil.typeOfParameter; +import static io.quarkus.deployment.util.ReflectUtil.unwrapInvocationTargetException; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +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.SortedSet; +import java.util.TreeMap; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.config.spi.Converter; +import org.jboss.logging.Logger; +import org.wildfly.common.Assert; + +import io.quarkus.deployment.configuration.definition.ClassDefinition; +import io.quarkus.deployment.configuration.definition.GroupDefinition; +import io.quarkus.deployment.configuration.definition.RootDefinition; +import io.quarkus.deployment.configuration.matching.ConfigPatternMap; +import io.quarkus.deployment.configuration.matching.Container; +import io.quarkus.deployment.configuration.matching.FieldContainer; +import io.quarkus.deployment.configuration.matching.MapContainer; +import io.quarkus.deployment.configuration.matching.PatternMapBuilder; +import io.quarkus.deployment.configuration.type.ArrayOf; +import io.quarkus.deployment.configuration.type.CollectionOf; +import io.quarkus.deployment.configuration.type.ConverterType; +import io.quarkus.deployment.configuration.type.Leaf; +import io.quarkus.deployment.configuration.type.LowerBoundCheckOf; +import io.quarkus.deployment.configuration.type.MinMaxValidated; +import io.quarkus.deployment.configuration.type.OptionalOf; +import io.quarkus.deployment.configuration.type.PatternValidated; +import io.quarkus.deployment.configuration.type.UpperBoundCheckOf; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.configuration.ExpandingConfigSource; +import io.quarkus.runtime.configuration.HyphenateEnumConverter; +import io.quarkus.runtime.configuration.NameIterator; +import io.smallrye.config.Converters; +import io.smallrye.config.SmallRyeConfig; + +/** + * A configuration reader. + */ +public final class BuildTimeConfigurationReader { + private static final Logger log = Logger.getLogger("io.quarkus.config.build"); + + final ConfigPatternMap buildTimePatternMap; + final ConfigPatternMap buildTimeRunTimePatternMap; + final ConfigPatternMap runTimePatternMap; + + final List buildTimeVisibleRoots; + final List allRoots; + + /** + * Construct a new instance. + * + * @param configRoots the configuration root class list (must not be {@code null}) + */ + public BuildTimeConfigurationReader(final List> configRoots) { + Assert.checkNotNullParam("configRoots", configRoots); + + List runTimeRoots = new ArrayList<>(); + List buildTimeRunTimeRoots = new ArrayList<>(); + List buildTimeRoots = new ArrayList<>(); + Map, GroupDefinition> groups = new HashMap<>(); + for (Class configRoot : configRoots) { + String name = ConfigItem.HYPHENATED_ELEMENT_NAME; + ConfigPhase phase = ConfigPhase.BUILD_TIME; + ConfigRoot annotation = configRoot.getAnnotation(ConfigRoot.class); + if (annotation != null) { + name = annotation.name(); + phase = annotation.phase(); + } + RootDefinition.Builder defBuilder = new RootDefinition.Builder(); + defBuilder.setConfigPhase(phase); + defBuilder.setRootName(name); + processClass(defBuilder, configRoot, groups); + RootDefinition definition = defBuilder.build(); + if (phase == ConfigPhase.BUILD_TIME) { + buildTimeRoots.add(definition); + } else if (phase == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { + buildTimeRunTimeRoots.add(definition); + } else { + assert phase == ConfigPhase.RUN_TIME; + runTimeRoots.add(definition); + } + } + + runTimePatternMap = PatternMapBuilder.makePatterns(runTimeRoots); + buildTimeRunTimePatternMap = PatternMapBuilder.makePatterns(buildTimeRunTimeRoots); + buildTimePatternMap = PatternMapBuilder.makePatterns(buildTimeRoots); + + buildTimeVisibleRoots = new ArrayList<>(buildTimeRoots.size() + buildTimeRunTimeRoots.size()); + buildTimeVisibleRoots.addAll(buildTimeRoots); + buildTimeVisibleRoots.addAll(buildTimeRunTimeRoots); + + List allRoots = new ArrayList<>(buildTimeVisibleRoots.size() + runTimeRoots.size()); + allRoots.addAll(buildTimeVisibleRoots); + allRoots.addAll(runTimeRoots); + + this.allRoots = allRoots; + } + + private static void processClass(ClassDefinition.Builder builder, Class clazz, + final Map, GroupDefinition> groups) { + builder.setConfigurationClass(clazz); + for (Field field : clazz.getDeclaredFields()) { + int mods = field.getModifiers(); + if (Modifier.isStatic(mods)) { + continue; + } + if (Modifier.isFinal(mods)) { + continue; + } + if (Modifier.isPrivate(mods)) { + throw reportError(field, "Configuration field may not be private"); + } + if (!Modifier.isPublic(mods) || !Modifier.isPublic(clazz.getModifiers())) { + field.setAccessible(true); + } + builder.addMember(processValue(field, field.getGenericType(), groups)); + } + } + + private static ClassDefinition.ClassMember.Specification processValue(Field field, Type valueType, + Map, GroupDefinition> groups) { + + Class valueClass = rawTypeOf(valueType); + final boolean isOptional = valueClass == Optional.class; + + if (valueClass == Map.class) { + if (!(valueType instanceof ParameterizedType)) { + throw reportError(field, "Map values must be parameterized"); + } + Class keyClass = rawTypeOfParameter(valueType, 0); + if (keyClass != String.class) { + throw reportError(field, "Map key types other than String are not yet supported"); + } + final ClassDefinition.ClassMember.Specification nested = processValue(field, typeOfParameter(valueType, 1), groups); + if (nested instanceof ClassDefinition.GroupMember.Specification + && ((ClassDefinition.GroupMember.Specification) nested).isOptional()) { + throw reportError(field, "Group map values may not be optional"); + } + return new ClassDefinition.MapMember.Specification(nested); + } else if (valueClass.getAnnotation(ConfigGroup.class) != null + || isOptional && rawTypeOfParameter(valueType, 0).getAnnotation(ConfigGroup.class) != null) { + Class groupClass; + if (isOptional) { + groupClass = rawTypeOfParameter(valueType, 0); + } else { + groupClass = valueClass; + } + GroupDefinition def = groups.get(groupClass); + if (def == null) { + final GroupDefinition.Builder subBuilder = new GroupDefinition.Builder(); + processClass(subBuilder, groupClass, groups); + groups.put(groupClass, def = subBuilder.build()); + } + return new ClassDefinition.GroupMember.Specification(field, def, isOptional); + } else { + final String defaultDefault; + // primitive values generally get their normal initializers as a default value + if (valueClass == boolean.class) { + defaultDefault = "false"; + } else if (valueClass.isPrimitive() && valueClass != char.class) { + defaultDefault = "0"; + } else { + defaultDefault = null; + } + ConfigItem configItem = field.getAnnotation(ConfigItem.class); + if (configItem != null) { + final String defaultVal = configItem.defaultValue(); + return new ClassDefinition.ItemMember.Specification(field, + defaultVal.equals(ConfigItem.NO_DEFAULT) ? defaultDefault : defaultVal); + } else { + ConfigProperty configProperty = field.getAnnotation(ConfigProperty.class); + if (configProperty != null) { + log.warnf("Using @ConfigProperty for Quarkus configuration items is deprecated " + + "(use @ConfigItem instead) at %s#%s", field.getDeclaringClass().getName(), field.getName()); + final String defaultVal = configProperty.defaultValue(); + return new ClassDefinition.ItemMember.Specification(field, + defaultVal.equals(ConfigProperty.UNCONFIGURED_VALUE) ? defaultDefault : defaultVal); + } else { + // todo: should we log a warning that there is no annotation for the property, or just allow it? + return new ClassDefinition.ItemMember.Specification(field, defaultDefault); + } + } + } + } + + public ConfigPatternMap getBuildTimePatternMap() { + return buildTimePatternMap; + } + + public ConfigPatternMap getBuildTimeRunTimePatternMap() { + return buildTimeRunTimePatternMap; + } + + public ConfigPatternMap getRunTimePatternMap() { + return runTimePatternMap; + } + + public List getBuildTimeVisibleRoots() { + return buildTimeVisibleRoots; + } + + public List getAllRoots() { + return allRoots; + } + + public ReadResult readConfiguration(final SmallRyeConfig config) { + return new ReadOperation(config).run(); + } + + final class ReadOperation { + final SmallRyeConfig config; + final Set processedNames = new HashSet<>(); + + final Map, Object> objectsByRootClass = new HashMap<>(); + final Map specifiedRunTimeDefaultValues = new TreeMap<>(); + final Map buildTimeRunTimeVisibleValues = new TreeMap<>(); + + final Map> convByType = new HashMap<>(); + + ReadOperation(final SmallRyeConfig config) { + this.config = config; + } + + ReadResult run() { + final StringBuilder nameBuilder; + nameBuilder = new StringBuilder().append("quarkus"); + // eager init first + int len = nameBuilder.length(); + for (RootDefinition root : buildTimeVisibleRoots) { + Class clazz = root.getConfigurationClass(); + Object instance; + try { + Constructor cons = clazz.getDeclaredConstructor(); + cons.setAccessible(true); + instance = cons.newInstance(); + } catch (InstantiationException e) { + throw toError(e); + } catch (IllegalAccessException e) { + throw toError(e); + } catch (InvocationTargetException e) { + throw unwrapInvocationTargetException(e); + } catch (NoSuchMethodException e) { + throw toError(e); + } + objectsByRootClass.put(clazz, instance); + String rootName = root.getRootName(); + if (!rootName.isEmpty()) { + nameBuilder.append('.').append(rootName); + } + readConfigGroup(root, instance, nameBuilder); + nameBuilder.setLength(len); + } + // sweep-up + for (String propertyName : config.getPropertyNames()) { + NameIterator ni = new NameIterator(propertyName); + if (ni.hasNext() && ni.nextSegmentEquals("quarkus")) { + ni.next(); + // build time patterns + Container matched = buildTimePatternMap.match(ni); + if (matched instanceof FieldContainer) { + ni.goToEnd(); + // cursor is located after group property key (if any) + getGroup((FieldContainer) matched, ni); + // we don't have to set the field because the group object init does it for us + continue; + } else if (matched != null) { + assert matched instanceof MapContainer; + // it's a leaf value within a map + // these must always be explicitly set + ni.goToEnd(); + // cursor is located after the map key + final String key = ni.getPreviousSegment(); + final Map map = getMap((MapContainer) matched, ni); + // we always have to set the map entry ourselves + Field field = matched.findField(); + Converter converter = getConverter(config, field, ConverterType.of(field)); + map.put(key, config.getValue(propertyName, converter)); + continue; + } + // build time (run time visible) patterns + ni.goToStart(); + ni.next(); + matched = buildTimeRunTimePatternMap.match(ni); + if (matched instanceof FieldContainer) { + ni.goToEnd(); + // cursor is located after group property key (if any) + getGroup((FieldContainer) matched, ni); + buildTimeRunTimeVisibleValues.put(propertyName, + config.getOptionalValue(propertyName, String.class).orElse("")); + continue; + } else if (matched != null) { + assert matched instanceof MapContainer; + // it's a leaf value within a map + // these must always be explicitly set + ni.goToEnd(); + // cursor is located after the map key + final String key = ni.getPreviousSegment(); + final Map map = getMap((MapContainer) matched, ni); + // we always have to set the map entry ourselves + Field field = matched.findField(); + Converter converter = getConverter(config, field, ConverterType.of(field)); + map.put(key, config.getValue(propertyName, converter)); + // cache the resolved value + buildTimeRunTimeVisibleValues.put(propertyName, + config.getOptionalValue(propertyName, String.class).orElse("")); + continue; + } + // run time patterns + ni.goToStart(); + ni.next(); + matched = runTimePatternMap.match(ni); + if (matched != null) { + // it's a specified run-time default (record for later) + boolean old = ExpandingConfigSource.setExpanding(false); + try { + specifiedRunTimeDefaultValues.put(propertyName, + config.getOptionalValue(propertyName, String.class).orElse("")); + } finally { + ExpandingConfigSource.setExpanding(old); + } + } + } else { + // it's not managed by us; record it + boolean old = ExpandingConfigSource.setExpanding(false); + try { + specifiedRunTimeDefaultValues.put(propertyName, + config.getOptionalValue(propertyName, String.class).orElse("")); + } finally { + ExpandingConfigSource.setExpanding(old); + } + } + } + return new ReadResult(objectsByRootClass, specifiedRunTimeDefaultValues, buildTimeRunTimeVisibleValues, + buildTimePatternMap, buildTimeRunTimePatternMap, runTimePatternMap, allRoots); + } + + /** + * Get a matched group. The tree node points to the property within the group that was matched. + * + * @param matched the matcher tree node + * @param ni the name iterator, positioned after the group member key (if any) + * @return the (possibly new) group instance + */ + private Object getGroup(FieldContainer matched, NameIterator ni) { + final ClassDefinition.ClassMember classMember = matched.getClassMember(); + ClassDefinition definition = matched.findEnclosingClass(); + Class configurationClass = definition.getConfigurationClass(); + if (definition instanceof RootDefinition) { + // found the root + return objectsByRootClass.get(configurationClass); + } + Container parent = matched.getParent(); + final boolean consume = !classMember.getPropertyName().isEmpty(); + if (consume) { + ni.previous(); + } + // now the cursor is *before* the group member key but after the base group + if (parent instanceof FieldContainer) { + FieldContainer parentClass = (FieldContainer) parent; + Field field = parentClass.findField(); + // the cursor is located after the enclosing group's property name (if any) + Object enclosing = getGroup(parentClass, ni); + // cursor restored to after group member key + if (consume) { + ni.next(); + } + if ((classMember instanceof ClassDefinition.GroupMember) + && ((ClassDefinition.GroupMember) classMember).isOptional()) { + Optional opt; + try { + opt = (Optional) field.get(enclosing); + } catch (IllegalAccessException e) { + throw toError(e); + } + if (opt.isPresent()) { + return opt.get(); + } else { + Object instance = recreateGroup(ni, definition, configurationClass); + try { + field.set(enclosing, Optional.of(instance)); + } catch (IllegalAccessException e) { + throw toError(e); + } + return instance; + } + } else { + try { + return field.get(enclosing); + } catch (IllegalAccessException e) { + throw toError(e); + } + } + } else { + assert parent instanceof MapContainer; + final MapContainer parentMap = (MapContainer) parent; + Map map = getMap(parentMap, ni); + // the base group is a map, so the previous segment is the key of the map + String key = ni.getPreviousSegment(); + Object instance = map.get(key); + if (instance == null) { + instance = recreateGroup(ni, definition, configurationClass); + map.put(key, instance); + } + // cursor restored to after group member key + if (consume) { + ni.next(); + } + return instance; + } + } + + /** + * Get a matched map. The tree node points to the position after the map key. + * + * @param matched the matcher tree node + * @param ni the name iterator, positioned just after the map key; restored on exit + * @return the map + */ + private Map getMap(MapContainer matched, NameIterator ni) { + Container parent = matched.getParent(); + if (parent instanceof FieldContainer) { + FieldContainer parentClass = (FieldContainer) parent; + Field field = parentClass.findField(); + ni.previous(); + // now the cursor is before our map key and after the enclosing group property (if any) + Object instance = getGroup(parentClass, ni); + ni.next(); + // cursor restored + try { + return getFieldAsMap(field, instance); + } catch (IllegalAccessException e) { + throw toError(e); + } + } else { + assert parent instanceof MapContainer; + ni.previous(); + // now the cursor is before our map key and after the enclosing map key + Map map = getMap((MapContainer) parent, ni); + String key = ni.getPreviousSegment(); + ni.next(); + // cursor restored + Map instance = getAsMap(map, key); + if (instance == null) { + instance = new HashMap<>(); + map.put(key, instance); + } + return instance; + } + } + + private Object recreateGroup(final NameIterator ni, final ClassDefinition definition, + final Class configurationClass) { + // re-create this config group + final Object instance; + try { + instance = configurationClass.getConstructor().newInstance(); + } catch (InstantiationException e) { + throw toError(e); + } catch (IllegalAccessException e) { + throw toError(e); + } catch (InvocationTargetException e) { + throw unwrapInvocationTargetException(e); + } catch (NoSuchMethodException e) { + throw toError(e); + } + // the name includes everything up to (but not including) the member key + final StringBuilder nameBuilder = new StringBuilder(ni.getAllPreviousSegments()); + readConfigGroup(definition, instance, nameBuilder); + return instance; + } + + @SuppressWarnings("unchecked") + private Map getAsMap(final Map map, final String key) { + return (Map) map.get(key); + } + + @SuppressWarnings("unchecked") + private Map getFieldAsMap(final Field field, final Object instance) throws IllegalAccessException { + return (Map) field.get(instance); + } + + /** + * Read a configuration group, recursing into nested groups and instantiating empty maps. + * + * @param definition the definition of the configuration group + * @param instance the group instance + * @param nameBuilder the name builder (set to the last segment before the current group's property names) + */ + private void readConfigGroup(ClassDefinition definition, Object instance, final StringBuilder nameBuilder) { + for (ClassDefinition.ClassMember member : definition.getMembers()) { + Field field = member.getField(); + if (member instanceof ClassDefinition.MapMember) { + // get these on the sweep-up + try { + field.set(instance, new TreeMap<>()); + } catch (IllegalAccessException e) { + throw toError(e); + } + continue; + } + String propertyName = member.getPropertyName(); + if (member instanceof ClassDefinition.ItemMember) { + ClassDefinition.ItemMember leafMember = (ClassDefinition.ItemMember) member; + int len = nameBuilder.length(); + try { + if (!propertyName.isEmpty()) { + nameBuilder.append('.').append(propertyName); + } + String fullName = nameBuilder.toString(); + if (processedNames.add(fullName)) { + readConfigValue(fullName, leafMember, instance); + } + } finally { + nameBuilder.setLength(len); + } + } else { + assert member instanceof ClassDefinition.GroupMember; + // construct the nested instance + ClassDefinition.GroupMember groupMember = (ClassDefinition.GroupMember) member; + if (groupMember.isOptional()) { + try { + field.set(instance, Optional.empty()); + } catch (IllegalAccessException e) { + throw toError(e); + } + } else { + Class clazz = groupMember.getGroupDefinition().getConfigurationClass(); + Object nestedInstance; + try { + nestedInstance = clazz.getConstructor().newInstance(); + } catch (InstantiationException e) { + throw toError(e); + } catch (InvocationTargetException e) { + throw unwrapInvocationTargetException(e); + } catch (NoSuchMethodException e) { + throw toError(e); + } catch (IllegalAccessException e) { + throw toError(e); + } + try { + field.set(instance, nestedInstance); + } catch (IllegalAccessException e) { + throw toError(e); + } + if (propertyName.isEmpty()) { + readConfigGroup( + groupMember.getGroupDefinition(), nestedInstance, nameBuilder); + } else { + int len = nameBuilder.length(); + try { + nameBuilder.append('.').append(propertyName); + readConfigGroup( + groupMember.getGroupDefinition(), nestedInstance, nameBuilder); + } finally { + nameBuilder.setLength(len); + } + } + } + } + } + } + + private void readConfigValue(String fullName, ClassDefinition.ItemMember member, Object instance) { + Field field = member.getField(); + Converter converter = getConverter(config, field, ConverterType.of(field)); + Object val = config.getValue(fullName, converter); + try { + field.set(instance, val); + } catch (IllegalAccessException e) { + throw toError(e); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Converter getConverter(SmallRyeConfig config, Field field, ConverterType valueType) { + Converter converter = convByType.get(valueType); + if (converter != null) { + return converter; + } + if (valueType instanceof ArrayOf) { + ArrayOf arrayOf = (ArrayOf) valueType; + converter = Converters.newArrayConverter( + getConverter(config, field, arrayOf.getElementType()), + arrayOf.getArrayType()); + } else if (valueType instanceof CollectionOf) { + CollectionOf collectionOf = (CollectionOf) valueType; + Class collectionClass = collectionOf.getCollectionClass(); + final Converter nested = getConverter(config, field, collectionOf.getElementType()); + if (collectionClass == List.class) { + converter = Converters.newCollectionConverter(nested, ConfigUtils.listFactory()); + } else if (collectionClass == Set.class) { + converter = Converters.newCollectionConverter(nested, ConfigUtils.setFactory()); + } else if (collectionClass == SortedSet.class) { + converter = Converters.newCollectionConverter(nested, ConfigUtils.sortedSetFactory()); + } else { + throw reportError(field, "Unsupported configuration collection type: %s", collectionClass); + } + } else if (valueType instanceof Leaf) { + Leaf leaf = (Leaf) valueType; + Class> convertWith = leaf.getConvertWith(); + if (convertWith != null) { + try { + final Constructor> ctor; + // TODO: temporary until type param inference is in + if (convertWith == HyphenateEnumConverter.class.asSubclass(Converter.class)) { + ctor = convertWith.getConstructor(Class.class); + converter = ctor.newInstance(valueType.getLeafType()); + } else { + ctor = convertWith.getConstructor(); + converter = ctor.newInstance(); + } + } catch (InstantiationException e) { + throw toError(e); + } catch (IllegalAccessException e) { + throw toError(e); + } catch (InvocationTargetException e) { + throw unwrapInvocationTargetException(e); + } catch (NoSuchMethodException e) { + throw toError(e); + } + } else { + converter = config.getConverter(leaf.getLeafType()); + } + } else if (valueType instanceof LowerBoundCheckOf) { + // todo: add in bounds checker + converter = getConverter(config, field, ((LowerBoundCheckOf) valueType).getClassConverterType()); + } else if (valueType instanceof UpperBoundCheckOf) { + // todo: add in bounds checker + converter = getConverter(config, field, ((UpperBoundCheckOf) valueType).getClassConverterType()); + } else if (valueType instanceof MinMaxValidated) { + MinMaxValidated minMaxValidated = (MinMaxValidated) valueType; + String min = minMaxValidated.getMin(); + boolean minInclusive = minMaxValidated.isMinInclusive(); + String max = minMaxValidated.getMax(); + boolean maxInclusive = minMaxValidated.isMaxInclusive(); + Converter nestedConverter = getConverter(config, field, minMaxValidated.getNestedType()); + if (min != null) { + if (max != null) { + converter = Converters.rangeValueStringConverter((Converter) nestedConverter, min, minInclusive, max, + maxInclusive); + } else { + converter = Converters.minimumValueStringConverter((Converter) nestedConverter, min, minInclusive); + } + } else { + assert min == null && max != null; + converter = Converters.maximumValueStringConverter((Converter) nestedConverter, max, maxInclusive); + } + } else if (valueType instanceof OptionalOf) { + OptionalOf optionalOf = (OptionalOf) valueType; + converter = Converters.newOptionalConverter(getConverter(config, field, optionalOf.getNestedType())); + } else if (valueType instanceof PatternValidated) { + PatternValidated patternValidated = (PatternValidated) valueType; + converter = Converters.patternValidatingConverter(getConverter(config, field, patternValidated.getNestedType()), + patternValidated.getPatternString()); + } else { + throw Assert.unreachableCode(); + } + convByType.put(valueType, converter); + return converter; + } + } + + public static final class ReadResult { + final Map, Object> objectsByRootClass; + final Map specifiedRunTimeDefaultValues; + final Map buildTimeRunTimeVisibleValues; + final ConfigPatternMap buildTimePatternMap; + final ConfigPatternMap buildTimeRunTimePatternMap; + final ConfigPatternMap runTimePatternMap; + final Map, RootDefinition> runTimeRootsByClass; + final List allRoots; + + ReadResult(final Map, Object> objectsByRootClass, final Map specifiedRunTimeDefaultValues, + final Map buildTimeRunTimeVisibleValues, + final ConfigPatternMap buildTimePatternMap, + final ConfigPatternMap buildTimeRunTimePatternMap, + final ConfigPatternMap runTimePatternMap, final List allRoots) { + this.objectsByRootClass = objectsByRootClass; + this.specifiedRunTimeDefaultValues = specifiedRunTimeDefaultValues; + this.buildTimeRunTimeVisibleValues = buildTimeRunTimeVisibleValues; + this.buildTimePatternMap = buildTimePatternMap; + this.buildTimeRunTimePatternMap = buildTimeRunTimePatternMap; + this.runTimePatternMap = runTimePatternMap; + this.allRoots = allRoots; + Map, RootDefinition> map = new HashMap<>(); + for (RootDefinition root : allRoots) { + map.put(root.getConfigurationClass(), root); + } + runTimeRootsByClass = map; + } + + public Map, Object> getObjectsByRootClass() { + return objectsByRootClass; + } + + public Object requireRootObjectForClass(Class clazz) { + Object obj = objectsByRootClass.get(clazz); + if (obj == null) { + throw new IllegalStateException("No root found for " + clazz); + } + return obj; + } + + public Map getSpecifiedRunTimeDefaultValues() { + return specifiedRunTimeDefaultValues; + } + + public Map getBuildTimeRunTimeVisibleValues() { + return buildTimeRunTimeVisibleValues; + } + + public ConfigPatternMap getBuildTimePatternMap() { + return buildTimePatternMap; + } + + public ConfigPatternMap getBuildTimeRunTimePatternMap() { + return buildTimeRunTimePatternMap; + } + + public ConfigPatternMap getRunTimePatternMap() { + return runTimePatternMap; + } + + public List getAllRoots() { + return allRoots; + } + + public RootDefinition requireRootDefinitionForClass(Class clazz) { + final RootDefinition def = runTimeRootsByClass.get(clazz); + if (def == null) { + throw new IllegalStateException("No root definition found for " + clazz); + } + return def; + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/CompoundConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/CompoundConfigType.java deleted file mode 100644 index 4cc3734a8d0d4..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/CompoundConfigType.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.quarkus.deployment.configuration; - -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - * A node which contains other nodes. - */ -public abstract class CompoundConfigType extends ConfigType { - CompoundConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment) { - super(containingName, container, consumeSegment); - } - - /** - * Get or create a child instance of this node. - * - * @param name the property name of the child instance (must not be {@code null}) - * @param cache - * @param config the configuration (must not be {@code null}) - * @param self the instance of this node (must not be {@code null}) - * @param childName the static child name, or {@code null} if the child name is dynamic - * @return the child instance - */ - abstract Object getChildObject(NameIterator name, final ExpandingConfigSource.Cache cache, SmallRyeConfig config, - Object self, String childName); - - abstract ResultHandle generateGetChildObject(BytecodeCreator body, ResultHandle name, final ResultHandle cache, - ResultHandle config, - ResultHandle self, String childName); - - /** - * Set a child object on the given instance. - * - * @param name the child property name iterator - * @param self the instance of this configuration type - * @param containingName the child property name - * @param value the child property value - */ - abstract void setChildObject(NameIterator name, Object self, String containingName, Object value); - - abstract void generateSetChildObject(BytecodeCreator body, ResultHandle name, ResultHandle self, String containingName, - ResultHandle value); - - /** - * Get or create the instance of this root, recursively adding it to its parent if necessary. - * - * @param name the name of this property node (must not be {@code null}) - * @param cache - * @param config the configuration (must not be {@code null}) - * @return the possibly new object instance - */ - abstract Object getOrCreate(NameIterator name, final ExpandingConfigSource.Cache cache, SmallRyeConfig config); - - abstract ResultHandle generateGetOrCreate(BytecodeCreator body, ResultHandle name, final ResultHandle cache, - ResultHandle config); - - abstract void acceptConfigurationValueIntoLeaf(LeafConfigType leafType, NameIterator name, - final ExpandingConfigSource.Cache cache, SmallRyeConfig config); - - abstract void generateAcceptConfigurationValueIntoLeaf(BytecodeCreator body, LeafConfigType leafType, ResultHandle name, - final ResultHandle cache, ResultHandle config); -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigDefinition.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigDefinition.java deleted file mode 100644 index 4fdd91a4c30ce..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigDefinition.java +++ /dev/null @@ -1,610 +0,0 @@ -package io.quarkus.deployment.configuration; - -import static io.quarkus.deployment.util.ReflectUtil.rawTypeOf; -import static io.quarkus.deployment.util.ReflectUtil.rawTypeOfParameter; -import static io.quarkus.deployment.util.ReflectUtil.typeOfParameter; -import static io.quarkus.runtime.util.StringUtil.camelHumpsIterator; -import static io.quarkus.runtime.util.StringUtil.hyphenate; -import static io.quarkus.runtime.util.StringUtil.join; -import static io.quarkus.runtime.util.StringUtil.lowerCase; -import static io.quarkus.runtime.util.StringUtil.lowerCaseFirst; -import static io.quarkus.runtime.util.StringUtil.withoutSuffix; - -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Field; -import java.lang.reflect.Member; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; -import java.util.Set; -import java.util.TreeMap; - -import org.eclipse.microprofile.config.spi.Converter; -import org.jboss.logging.Logger; -import org.objectweb.asm.Opcodes; -import org.wildfly.common.Assert; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.ClassOutput; -import io.quarkus.gizmo.DescriptorUtils; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; -import io.quarkus.runtime.annotations.ConvertWith; -import io.quarkus.runtime.annotations.DefaultConverter; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.HyphenateEnumConverter; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - * A configuration definition. This class represents the configuration space as trees of nodes, where each tree - * has a root which recursively contains all of the elements within the configuration. - */ -public class ConfigDefinition extends CompoundConfigType { - private static final Logger log = Logger.getLogger("io.quarkus.config"); - - public static final String NO_CONTAINING_NAME = "<>"; - - private static final String QUARKUS_NAMESPACE = "quarkus"; - - // for now just list the values manually - private static final List FALSE_POSITIVE_QUARKUS_CONFIG_MISSES = Arrays - .asList(QUARKUS_NAMESPACE + ".live-reload.password", QUARKUS_NAMESPACE + ".live-reload.url", - QUARKUS_NAMESPACE + ".debug.generated-classes-dir", QUARKUS_NAMESPACE + ".debug.reflection", - QUARKUS_NAMESPACE + ".build.skip", - QUARKUS_NAMESPACE + ".platform.group-id", - QUARKUS_NAMESPACE + ".platform.artifact-id", - QUARKUS_NAMESPACE + ".platform.version", - QUARKUS_NAMESPACE + ".version", QUARKUS_NAMESPACE + ".profile", QUARKUS_NAMESPACE + ".test.profile", - QUARKUS_NAMESPACE + ".test.native-image-wait-time", - QUARKUS_NAMESPACE + ".test.native-image-profile"); - - private final TreeMap rootObjectsByContainingName = new TreeMap<>(); - private final HashMap, Object> rootObjectsByClass = new HashMap<>(); - private final ConfigPatternMap leafPatterns = new ConfigPatternMap<>(); - private final IdentityHashMap realizedInstances = new IdentityHashMap<>(); - private final TreeMap rootTypesByContainingName = new TreeMap<>(); - private final FieldDescriptor rootField; - private final TreeMap loadedProperties = new TreeMap<>(); - private final boolean deferResolution; - - public ConfigDefinition(final FieldDescriptor rootField, final boolean deferResolution) { - super(null, null, false); - this.deferResolution = deferResolution; - Assert.checkNotNullParam("rootField", rootField); - this.rootField = rootField; - } - - public ConfigDefinition(final FieldDescriptor rootField) { - this(rootField, false); - } - - void acceptConfigurationValueIntoLeaf(final LeafConfigType leafType, final NameIterator name, - final ExpandingConfigSource.Cache cache, final SmallRyeConfig config) { - // primitive/leaf values without a config group - throw Assert.unsupported(); - } - - void generateAcceptConfigurationValueIntoLeaf(final BytecodeCreator body, final LeafConfigType leafType, - final ResultHandle name, final ResultHandle cache, final ResultHandle config) { - // primitive/leaf values without a config group - throw Assert.unsupported(); - } - - Object getChildObject(final NameIterator name, final ExpandingConfigSource.Cache cache, final SmallRyeConfig config, - final Object self, final String childName) { - return rootObjectsByContainingName.get(childName); - } - - ResultHandle generateGetChildObject(final BytecodeCreator body, final ResultHandle name, final ResultHandle cache, - final ResultHandle config, - final ResultHandle self, final String childName) { - return body.readInstanceField(rootTypesByContainingName.get(childName).getFieldDescriptor(), self); - } - - TreeMap getOrCreate(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - return rootObjectsByContainingName; - } - - ResultHandle generateGetOrCreate(final BytecodeCreator body, final ResultHandle name, final ResultHandle cache, - final ResultHandle config) { - return body.readStaticField(rootField); - } - - void setChildObject(final NameIterator name, final Object self, final String childName, final Object value) { - if (self != rootObjectsByContainingName) - throw new IllegalStateException("Wrong self pointer: " + self); - final RootInfo rootInfo = rootTypesByContainingName.get(childName); - assert rootInfo != null : "Unknown child: " + childName; - assert !rootObjectsByContainingName.containsKey(childName) : "Child added twice: " + childName; - rootObjectsByContainingName.put(childName, value); - rootObjectsByClass.put(rootInfo.getRootClass(), value); - realizedInstances.put(value, new ValueInfo(childName, rootInfo)); - } - - void generateSetChildObject(final BytecodeCreator body, final ResultHandle name, final ResultHandle self, - final String containingName, final ResultHandle value) { - // objects should always be pre-initialized - throw Assert.unsupported(); - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - throw Assert.unsupported(); - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - throw Assert.unsupported(); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - throw Assert.unsupported(); - } - - public void load() { - loadFrom(leafPatterns); - } - - public void initialize(final SmallRyeConfig config, final ExpandingConfigSource.Cache cache) { - for (Map.Entry entry : rootTypesByContainingName.entrySet()) { - final RootInfo rootInfo = entry.getValue(); - // name iterator and config are always ignored because no root types are ever stored in a map node and no conversion is ever done - // TODO: make a separate create method for root types just to avoid this kind of thing - rootInfo.getRootType().getOrCreate(new NameIterator("ignored", true), cache, config); - } - } - - public void registerConfigRoot(Class configRoot) { - final AccessorFinder accessorFinder = new AccessorFinder(); - final ConfigRoot configRootAnnotation = configRoot.getAnnotation(ConfigRoot.class); - final ConfigPhase configPhase = configRootAnnotation.phase(); - if (configRoot.isAnnotationPresent(ConfigGroup.class)) { - throw reportError(configRoot, "Roots cannot have a @ConfigGroup annotation"); - } - final String containingName; - if (configPhase == ConfigPhase.RUN_TIME) { - containingName = join( - withoutSuffix(lowerCaseFirst(camelHumpsIterator(configRoot.getSimpleName())), "Config", "Configuration", - "RunTimeConfig", "RunTimeConfiguration")); - } else { - containingName = join( - withoutSuffix(lowerCaseFirst(camelHumpsIterator(configRoot.getSimpleName())), "Config", "Configuration", - "BuildTimeConfig", "BuildTimeConfiguration")); - } - final String name = configRootAnnotation.name(); - final String rootName; - if (name.equals(ConfigItem.PARENT)) { - throw reportError(configRoot, "Root cannot inherit parent name because it has no parent"); - } else if (name.equals(ConfigItem.ELEMENT_NAME)) { - rootName = containingName; - } else if (name.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) { - rootName = join("-", - withoutSuffix(lowerCase(camelHumpsIterator(configRoot.getSimpleName())), "config", "configuration")); - } else { - rootName = name; - } - if (rootTypesByContainingName.containsKey(containingName)) - throw reportError(configRoot, "Duplicate configuration root name \"" + containingName + "\""); - final GroupConfigType configGroup = processConfigGroup(containingName, this, true, rootName, configRoot, - accessorFinder); - final RootInfo rootInfo = new RootInfo(configRoot, configGroup, FieldDescriptor - .of(DescriptorUtils.getTypeStringFromDescriptorFormat(rootField.getType()), containingName, Object.class), - configPhase); - rootTypesByContainingName.put(containingName, rootInfo); - } - - private GroupConfigType processConfigGroup(final String containingName, final CompoundConfigType container, - final boolean consumeSegment, final String baseKey, final Class configGroupClass, - final AccessorFinder accessorFinder) { - GroupConfigType gct = new GroupConfigType(containingName, container, consumeSegment, configGroupClass, accessorFinder); - final Field[] fields = configGroupClass.getDeclaredFields(); - for (Field field : fields) { - String javadocKey = field.getDeclaringClass().getName().replace("$", ".") + "." + field.getName(); - final int mods = field.getModifiers(); - if (Modifier.isStatic(mods)) { - // ignore static fields - continue; - } - if (Modifier.isFinal(mods)) { - // ignore final fields - continue; - } - final ConfigItem configItemAnnotation = field.getAnnotation(ConfigItem.class); - final String name = configItemAnnotation == null ? hyphenate(field.getName()) : configItemAnnotation.name(); - String subKey; - boolean consume; - if (name.equals(ConfigItem.PARENT)) { - subKey = baseKey; - consume = false; - } else if (name.equals(ConfigItem.ELEMENT_NAME)) { - subKey = baseKey + "." + field.getName(); - consume = true; - } else if (name.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) { - subKey = baseKey + "." + hyphenate(field.getName()); - consume = true; - } else { - subKey = baseKey + "." + name; - consume = true; - } - final String defaultValue = configItemAnnotation == null ? ConfigItem.NO_DEFAULT - : configItemAnnotation.defaultValue(); - final Type fieldType = field.getGenericType(); - final Class fieldClass = field.getType(); - if (fieldClass.isAnnotationPresent(ConfigGroup.class)) { - if (!defaultValue.equals(ConfigItem.NO_DEFAULT)) { - throw reportError(field, "Unsupported default value"); - } - gct.addField(processConfigGroup(field.getName(), gct, consume, subKey, fieldClass, accessorFinder)); - } else if (fieldClass.isPrimitive()) { - final LeafConfigType leaf; - if (fieldClass == boolean.class) { - gct.addField(leaf = new BooleanConfigType(field.getName(), gct, consume, - defaultValue.equals(ConfigItem.NO_DEFAULT) ? "false" : defaultValue, javadocKey, subKey, - loadEnhancedConverter(field, Boolean.class, subKey))); - } else if (fieldClass == int.class) { - gct.addField(leaf = new IntConfigType(field.getName(), gct, consume, - defaultValue.equals(ConfigItem.NO_DEFAULT) ? "0" : defaultValue, javadocKey, subKey, - loadEnhancedConverter(field, Integer.class, subKey))); - } else if (fieldClass == long.class) { - gct.addField(leaf = new LongConfigType(field.getName(), gct, consume, - defaultValue.equals(ConfigItem.NO_DEFAULT) ? "0" : defaultValue, javadocKey, subKey, - loadEnhancedConverter(field, Long.class, subKey))); - } else if (fieldClass == double.class) { - gct.addField(leaf = new DoubleConfigType(field.getName(), gct, consume, - defaultValue.equals(ConfigItem.NO_DEFAULT) ? "0" : defaultValue, javadocKey, subKey, - loadEnhancedConverter(field, Double.class, subKey))); - } else if (fieldClass == float.class) { - gct.addField(leaf = new FloatConfigType(field.getName(), gct, consume, - defaultValue.equals(ConfigItem.NO_DEFAULT) ? "0" : defaultValue, javadocKey, subKey, - loadEnhancedConverter(field, Float.class, subKey))); - } else { - throw reportError(field, "Unsupported primitive field type"); - } - container.getConfigDefinition().getLeafPatterns().addPattern(subKey, leaf); - } else if (fieldClass == Map.class) { - if (rawTypeOfParameter(fieldType, 0) != String.class) { - throw reportError(field, "Map key must be " + String.class); - } - - Type mapValueType = typeOfParameter(fieldType, 1); - Class mapValueRawType = rawTypeOf(mapValueType); - addMapField(field, gct, consume, subKey, mapValueType, accessorFinder, javadocKey, mapValueRawType); - } else if (fieldClass == List.class) { - // list leaf class - final LeafConfigType leaf; - ObjectListConfigType objectListConfigType = newObjectListConfigType(field, gct, consume, defaultValue, - javadocKey, subKey); - gct.addField(leaf = objectListConfigType); - container.getConfigDefinition().getLeafPatterns().addPattern(subKey, leaf); - } else if (fieldClass == Optional.class) { - final LeafConfigType leaf; - // optional config property - OptionalObjectConfigType optionalObjectConfigType = newOptionalObjectConfigType(field, gct, consume, - defaultValue, javadocKey, subKey); - gct.addField(leaf = optionalObjectConfigType); - container.getConfigDefinition().getLeafPatterns().addPattern(subKey, leaf); - } else { - final LeafConfigType leaf; - // it's a plain config property - ObjectConfigType objectConfigType = newObjectConfigType(field, gct, consume, defaultValue, javadocKey, - subKey); - gct.addField(leaf = objectConfigType); - container.getConfigDefinition().getLeafPatterns().addPattern(subKey, leaf); - } - } - return gct; - } - - private void addMapField(Field field, GroupConfigType gct, boolean consume, String subKey, Type mapValueType, - AccessorFinder accessorFinder, String javadocKey, Class mapValueRawType) { - final Class> converterClass = loadEnhancedConverter(field, mapValueRawType, subKey); - gct.addField(processMap(field.getName(), gct, field, consume, subKey, mapValueType, accessorFinder, javadocKey, - converterClass)); - } - - private ObjectConfigType newObjectConfigType(Field field, GroupConfigType gct, boolean consume, String defaultValue, - String javadocKey, String subKey) { - @SuppressWarnings("unchecked") - Class fieldClass = (Class) field.getType(); - return new ObjectConfigType<>(field.getName(), gct, consume, - mapDefaultValue(defaultValue, fieldClass), fieldClass, javadocKey, subKey, - loadEnhancedConverter(field, fieldClass, subKey)); - } - - private OptionalObjectConfigType newOptionalObjectConfigType(Field field, GroupConfigType gct, boolean consume, - String defaultValue, String javadocKey, String subKey) { - @SuppressWarnings("unchecked") - final Class optionalType = (Class) rawTypeOfParameter(field.getGenericType(), 0); - return new OptionalObjectConfigType<>(field.getName(), gct, consume, - defaultValue.equals(ConfigItem.NO_DEFAULT) ? "" : defaultValue, optionalType, javadocKey, subKey, - loadEnhancedConverter(field, optionalType, subKey)); - } - - private ObjectListConfigType newObjectListConfigType(Field field, GroupConfigType gct, boolean consume, - String defaultValue, String javadocKey, String subKey) { - @SuppressWarnings("unchecked") - final Class listType = (Class) rawTypeOfParameter(field.getGenericType(), 0); - return new ObjectListConfigType<>(field.getName(), gct, consume, mapDefaultValue(defaultValue, listType), listType, - javadocKey, subKey, loadEnhancedConverter(field, listType, subKey)); - } - - private Class> loadEnhancedConverter(Field field, Class clazz, String configProperty) { - final DefaultConverter defaultConverter = field.getAnnotation(DefaultConverter.class); - final ConvertWith convertWith = field.getAnnotation(ConvertWith.class); - - if (defaultConverter != null && convertWith != null) { - throw new IllegalArgumentException(String.format( - "Duplicate conversion behaviour specified on property %s : %s annotation and %s annotation given", - configProperty, DefaultConverter.class.getName(), ConvertWith.class.getName())); - } - - if (defaultConverter != null) { - return null; // use built in MP converters or custom converters - } - - if (convertWith != null) { - @SuppressWarnings("unchecked") - final Class> converterClass = (Class>) convertWith.value(); - try { - final Method method = converterClass.getMethod("convert", String.class); - final Type type = method.getAnnotatedReturnType().getType(); - if (clazz.isAssignableFrom(rawTypeOf(type))) { - return converterClass; - } - throw new IllegalArgumentException(String.format( - "Invalid converter supplied. Cannot convert %s to %s using the given converter %s", - configProperty, clazz, converterClass)); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException(e); - } - } - - if (clazz.isEnum()) { - // clean up with SmallRye Config upgrade - @SuppressWarnings({ "unchecked", "RedundantCast" }) - final Class> converterClass = (Class>) (Class) HyphenateEnumConverter.class; - return converterClass; - } - - return null; // use built in MP converters or custom converters - } - - private MapConfigType processMap(final String containingName, final CompoundConfigType container, - final AnnotatedElement containingElement, final boolean consumeSegment, final String baseKey, - final Type mapValueType, final AccessorFinder accessorFinder, String javadocKey, - Class> converterClass) { - MapConfigType mct = new MapConfigType(containingName, container, consumeSegment); - final Class valueClass = rawTypeOf(mapValueType); - final String subKey = baseKey + ".{*}"; - if (valueClass == Map.class) { - if (!(mapValueType instanceof ParameterizedType)) - throw reportError(containingElement, "Map must be parameterized"); - processMap(NO_CONTAINING_NAME, mct, containingElement, true, subKey, typeOfParameter(mapValueType, 1), - accessorFinder, javadocKey, converterClass); - } else if (valueClass.isAnnotationPresent(ConfigGroup.class)) { - processConfigGroup(NO_CONTAINING_NAME, mct, true, subKey, valueClass, accessorFinder); - } else if (valueClass == List.class) { - if (!(mapValueType instanceof ParameterizedType)) - throw reportError(containingElement, "List must be parameterized"); - @SuppressWarnings("unchecked") - Class listType = (Class) rawTypeOfParameter(mapValueType, 0); - final ObjectListConfigType leaf = new ObjectListConfigType<>(NO_CONTAINING_NAME, mct, consumeSegment, "", - listType, javadocKey, subKey, converterClass); - container.getConfigDefinition().getLeafPatterns().addPattern(subKey, leaf); - } else if (valueClass == Optional.class || valueClass == OptionalInt.class || valueClass == OptionalDouble.class - || valueClass == OptionalLong.class) { - throw reportError(containingElement, "Optionals are not allowed as a map value type"); - } else { - // treat as a plain object - @SuppressWarnings("unchecked") - final ObjectConfigType leaf = new ObjectConfigType<>(NO_CONTAINING_NAME, mct, true, "", (Class) valueClass, - javadocKey, subKey, converterClass); - container.getConfigDefinition().getLeafPatterns().addPattern(subKey, leaf); - } - return mct; - } - - private String mapDefaultValue(String defaultValue, Class fieldClass) { - String mappedDefault = defaultValue; - if (defaultValue.equals(ConfigItem.NO_DEFAULT)) { - if (Number.class.isAssignableFrom(fieldClass)) { - mappedDefault = "0"; - } else { - mappedDefault = ""; - } - } - return mappedDefault; - } - - private static IllegalArgumentException reportError(AnnotatedElement e, String msg) { - if (e instanceof Member) { - return new IllegalArgumentException(msg + " at " + e + " of " + ((Member) e).getDeclaringClass()); - } else if (e instanceof Parameter) { - return new IllegalArgumentException(msg + " at " + e + " of " + ((Parameter) e).getDeclaringExecutable() + " of " - + ((Parameter) e).getDeclaringExecutable().getDeclaringClass()); - } else { - return new IllegalArgumentException(msg + " at " + e); - } - } - - public void generateConfigRootClass(ClassOutput classOutput, AccessorFinder accessorFinder) { - try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput) - .className(DescriptorUtils.getTypeStringFromDescriptorFormat(rootField.getType())).superClass(Object.class) - .build()) { - try (MethodCreator ctor = cc.getMethodCreator("", void.class, SmallRyeConfig.class)) { - ctor.setModifiers(Opcodes.ACC_PUBLIC); - final ResultHandle self = ctor.getThis(); - final ResultHandle config = ctor.getMethodParam(0); - ctor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), self); - final ResultHandle cache = ctor.newInstance(ECS_CACHE_CTOR); - // initialize all fields to defaults - for (RootInfo value : rootTypesByContainingName.values()) { - if (value.getConfigPhase().isAvailableAtRun()) { - final CompoundConfigType rootType = value.getRootType(); - final String containingName = rootType.getContainingName(); - final FieldDescriptor fieldDescriptor = cc.getFieldCreator(containingName, Object.class) - .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL).getFieldDescriptor(); - ctor.writeInstanceField(fieldDescriptor, self, - rootType.writeInitialization(ctor, accessorFinder, cache, config)); - } - } - ctor.returnValue(null); - } - } - } - - public static void loadConfiguration(final ExpandingConfigSource.Cache cache, SmallRyeConfig config, - final Set unmatched, - ConfigDefinition... definitions) { - for (ConfigDefinition definition : definitions) { - definition.initialize(config, cache); - } - outer: for (String propertyName : config.getPropertyNames()) { - final NameIterator name = new NameIterator(propertyName); - if (name.hasNext()) { - if (name.nextSegmentEquals(QUARKUS_NAMESPACE)) { - name.next(); - for (ConfigDefinition definition : definitions) { - final LeafConfigType leafType = definition.leafPatterns.match(name); - if (leafType != null) { - name.goToEnd(); - final String nameString = name.toString(); - if (definition.deferResolution) { - boolean old = ExpandingConfigSource.setExpanding(false); - try { - leafType.acceptConfigurationValue(name, cache, config); - definition.loadedProperties.put(nameString, - config.getOptionalValue(nameString, String.class).orElse("")); - } finally { - ExpandingConfigSource.setExpanding(old); - } - } else { - leafType.acceptConfigurationValue(name, cache, config); - definition.loadedProperties.put(nameString, - config.getOptionalValue(nameString, String.class).orElse("")); - } - continue outer; - } - } - for (String entry : FALSE_POSITIVE_QUARKUS_CONFIG_MISSES) { - if (propertyName.equals(entry)) { - continue outer; - } - } - log.warnf("Unrecognized configuration key \"%s\" provided", propertyName); - } else { - // non-Quarkus value; capture it in the unmatched map for storage as a default value - unmatched.add(propertyName); - } - } - } - } - - public ConfigPatternMap getLeafPatterns() { - return leafPatterns; - } - - public ConfigDefinition getConfigDefinition() { - return this; - } - - public TreeMap getLoadedProperties() { - return loadedProperties; - } - - private void loadFrom(ConfigPatternMap map) { - final LeafConfigType matched = map.getMatched(); - if (matched != null) { - matched.load(); - } - for (String name : map.childNames()) { - loadFrom(map.getChild(name)); - } - } - - public Object getRealizedInstance(final Class rootClass) { - final Object obj = rootObjectsByClass.get(rootClass); - if (obj == null) { - throw new IllegalArgumentException("Unknown root class: " + rootClass); - } - return obj; - } - - public RootInfo getInstanceInfo(final Object obj) { - final ValueInfo valueInfo = realizedInstances.get(obj); - if (valueInfo == null) - return null; - return valueInfo.getRootInfo(); - } - - public static final class RootInfo { - private final Class rootClass; - private final GroupConfigType rootType; - private final FieldDescriptor fieldDescriptor; - private final ConfigPhase configPhase; - - RootInfo(final Class rootClass, final GroupConfigType rootType, final FieldDescriptor fieldDescriptor, - final ConfigPhase configPhase) { - this.rootClass = rootClass; - this.rootType = rootType; - this.fieldDescriptor = fieldDescriptor; - this.configPhase = configPhase; - } - - public Class getRootClass() { - return rootClass; - } - - public GroupConfigType getRootType() { - return rootType; - } - - public FieldDescriptor getFieldDescriptor() { - return fieldDescriptor; - } - - public ConfigPhase getConfigPhase() { - return configPhase; - } - } - - static final class ValueInfo { - private final String key; - private final RootInfo rootInfo; - - ValueInfo(final String key, final RootInfo rootInfo) { - this.key = key; - this.rootInfo = rootInfo; - } - - String getKey() { - return key; - } - - RootInfo getRootInfo() { - return rootInfo; - } - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigType.java deleted file mode 100644 index e5f744b32c035..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigType.java +++ /dev/null @@ -1,130 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Optional; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public abstract class ConfigType { - static final MethodDescriptor NI_PREV_METHOD = MethodDescriptor.ofMethod(NameIterator.class, "previous", void.class); - - static final MethodDescriptor NI_NEXT_METHOD = MethodDescriptor.ofMethod(NameIterator.class, "next", void.class); - - static final MethodDescriptor NI_GET_NEXT_SEGMENT = MethodDescriptor.ofMethod(NameIterator.class, "getNextSegment", - String.class); - - static final MethodDescriptor OBJ_TO_STRING_METHOD = MethodDescriptor.ofMethod(Object.class, "toString", String.class); - - static final MethodDescriptor OPT_OR_ELSE_METHOD = MethodDescriptor.ofMethod(Optional.class, "orElse", Object.class, - Object.class); - - static final MethodDescriptor OPT_OF_NULLABLE_METHOD = MethodDescriptor.ofMethod(Optional.class, "ofNullable", - Optional.class, Object.class); - - static final MethodDescriptor OPT_EMPTY_METHOD = MethodDescriptor.ofMethod(Optional.class, "empty", Optional.class); - - static final MethodDescriptor MAP_PUT_METHOD = MethodDescriptor.ofMethod(Map.class, "put", Object.class, Object.class, - Object.class); - - static final MethodDescriptor ECS_CACHE_CTOR = MethodDescriptor.ofConstructor(ExpandingConfigSource.Cache.class); - - /** - * Containing name. This is a field name or a map key, not a configuration key segment; as such, it is - * never {@code null} unless the containing name is intentionally dynamic. - */ - private final String containingName; - /** - * The containing node, or {@code null} if the node is a root. - */ - private final CompoundConfigType container; - /** - * Consume a segment of the name when traversing this node. Always {@code true} if the containing name is dynamic, - * otherwise only {@code true} if the node is a configuration group node with an empty relative name. - */ - private final boolean consumeSegment; - - ConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment) { - this.containingName = containingName; - this.container = container; - this.consumeSegment = consumeSegment; - } - - static IllegalAccessError toError(final IllegalAccessException e) { - IllegalAccessError e2 = new IllegalAccessError(e.getMessage()); - e2.setStackTrace(e.getStackTrace()); - return e2; - } - - static InstantiationError toError(final InstantiationException e) { - InstantiationError e2 = new InstantiationError(e.getMessage()); - e2.setStackTrace(e.getStackTrace()); - return e2; - } - - public String getContainingName() { - return containingName; - } - - public CompoundConfigType getContainer() { - return container; - } - - public T getContainer(Class expect) { - final CompoundConfigType container = getContainer(); - if (expect.isInstance(container)) - return expect.cast(container); - throw new IllegalStateException( - "Container is not a supported type; expected " + expect + " but got " + container.getClass()); - } - - public boolean isConsumeSegment() { - return consumeSegment; - } - - /** - * Load all configuration classes to enable configuration to be instantiated. - * - * @throws ClassNotFoundException if a required class was not found - */ - public abstract void load() throws ClassNotFoundException; - - /** - * A reusable method which returns an exception that can be thrown when a configuration - * node is used without its class being loaded. - * - * @return the not-loaded exception - */ - protected static IllegalStateException notLoadedException() { - return new IllegalStateException("Configuration tree classes not loaded"); - } - - /** - * Get the default value of this type into the enclosing element. - * - * @param enclosing the instance of the enclosing type (must not be {@code null}) - * @param cache - * @param config the configuration (must not be {@code null}) - * @param field the field to read the value into - */ - abstract void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field); - - abstract void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config); - - public abstract ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig); - - public ConfigDefinition getConfigDefinition() { - return container.getConfigDefinition(); - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/DefaultValuesConfigurationSource.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/DefaultValuesConfigurationSource.java index a4b851c0d2b00..289f7d4d74a70 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/DefaultValuesConfigurationSource.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/DefaultValuesConfigurationSource.java @@ -5,13 +5,17 @@ import org.eclipse.microprofile.config.spi.ConfigSource; +import io.quarkus.deployment.configuration.definition.ClassDefinition; +import io.quarkus.deployment.configuration.matching.ConfigPatternMap; +import io.quarkus.deployment.configuration.matching.Container; + /** * */ public class DefaultValuesConfigurationSource implements ConfigSource { - private final ConfigPatternMap leafs; + private final ConfigPatternMap leafs; - public DefaultValuesConfigurationSource(final ConfigPatternMap leafs) { + public DefaultValuesConfigurationSource(final ConfigPatternMap leafs) { this.leafs = leafs; } @@ -20,15 +24,19 @@ public Map getProperties() { } public String getValue(final String propertyName) { - final LeafConfigType match = leafs.match(propertyName); - if (match == null) { + if (!propertyName.startsWith("quarkus.")) { return null; } - final String defaultValueString = match.getDefaultValueString(); - if (defaultValueString == null || defaultValueString.isEmpty()) { + final Container match = leafs.match(propertyName.substring(8)); + if (match == null) { return null; } - return defaultValueString; + final ClassDefinition.ClassMember member = match.getClassMember(); + if (member instanceof ClassDefinition.ItemMember) { + final ClassDefinition.ItemMember leafMember = (ClassDefinition.ItemMember) member; + return leafMember.getDefaultValue(); + } + return null; } public String getName() { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/DoubleConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/DoubleConfigType.java deleted file mode 100644 index 709ed9210176e..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/DoubleConfigType.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; - -import org.eclipse.microprofile.config.spi.Converter; -import org.wildfly.common.Assert; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.deployment.steps.ConfigurationSetup; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class DoubleConfigType extends LeafConfigType { - private static final MethodDescriptor DOUBLE_VALUE_METHOD = MethodDescriptor.ofMethod(Double.class, "doubleValue", - double.class); - - final String defaultValue; - private final Class> converterClass; - - public DoubleConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final String defaultValue, String javadocKey, String configKey, Class> converterClass) { - super(containingName, container, consumeSegment, javadocKey, configKey); - Assert.checkNotEmptyParam("defaultValue", defaultValue); - this.defaultValue = defaultValue; - this.converterClass = converterClass; - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - name.previous(); - container.acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - public void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - Double value = ConfigUtils.getValue(config, name.toString(), Double.class, converterClass); - field.setDouble(enclosing, value != null ? value.doubleValue() : 0d); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - public void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - // final Double doubleValue = ConfigUtils.getValue(config, name.toString(), Double.class, converterClass); - // final double d = doubleValue != null ? doubleValue.doubleValue() : 0d; - final AssignableResultHandle result = body.createVariable(double.class); - final ResultHandle doubleValue = body.checkCast(body.invokeStaticMethod( - CU_GET_VALUE, - config, - body.invokeVirtualMethod( - OBJ_TO_STRING_METHOD, - name), - body.loadClass(Double.class), loadConverterClass(body)), Double.class); - final BranchResult ifNull = body.ifNull(doubleValue); - final BytecodeCreator isNull = ifNull.trueBranch(); - isNull.assign(result, isNull.load(0d)); - final BytecodeCreator isNotNull = ifNull.falseBranch(); - isNotNull.assign(result, - isNotNull.invokeVirtualMethod( - DOUBLE_VALUE_METHOD, - doubleValue)); - body.invokeStaticMethod(setter, enclosing, result); - } - - public String getDefaultValueString() { - return defaultValue; - } - - @Override - public Class getItemClass() { - return double.class; - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - Double value = ConfigUtils.convert(config, - ExpandingConfigSource.expandValue(defaultValue, cache), Double.class, converterClass); - field.setDouble(enclosing, value != null ? value.doubleValue() : 0d); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - body.invokeStaticMethod(setter, enclosing, - body.invokeVirtualMethod(DOUBLE_VALUE_METHOD, getConvertedDefault(body, cache, config))); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - return body.invokeVirtualMethod(DOUBLE_VALUE_METHOD, getConvertedDefault(body, cache, smallRyeConfig)); - } - - private ResultHandle getConvertedDefault(final BytecodeCreator body, final ResultHandle cache, final ResultHandle config) { - return body.invokeStaticMethod( - CU_CONVERT, - config, - cache == null ? body.load(defaultValue) - : body.invokeStaticMethod( - ConfigurationSetup.ECS_EXPAND_VALUE, - body.load(defaultValue), - cache), - body.loadClass(Double.class), loadConverterClass(body)); - } - - @Override - public Class> getConverterClass() { - return converterClass; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/FloatConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/FloatConfigType.java deleted file mode 100644 index 70bf1c039ffa2..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/FloatConfigType.java +++ /dev/null @@ -1,138 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; - -import org.eclipse.microprofile.config.spi.Converter; -import org.wildfly.common.Assert; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.deployment.steps.ConfigurationSetup; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class FloatConfigType extends LeafConfigType { - - private static final MethodDescriptor FLOAT_VALUE_METHOD = MethodDescriptor.ofMethod(Float.class, "floatValue", - float.class); - - final String defaultValue; - private final Class> converterClass; - - public FloatConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final String defaultValue, String javadocKey, String configKey, Class> converterClass) { - super(containingName, container, consumeSegment, javadocKey, configKey); - Assert.checkNotEmptyParam("defaultValue", defaultValue); - this.defaultValue = defaultValue; - this.converterClass = converterClass; - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - name.previous(); - container.acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - public void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - final Float value = ConfigUtils.getValue(config, name.toString(), Float.class, converterClass); - field.setFloat(enclosing, value != null ? value.floatValue() : 0f); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - public void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - // final Float floatValue = ConfigUtils.getValue(config, name.toString(), Float.class, converterClass); - // final float f = floatValue != null ? floatValue.floatValue() : 0f; - final AssignableResultHandle result = body.createVariable(float.class); - final ResultHandle floatValue = body.checkCast(body.invokeStaticMethod( - CU_GET_VALUE, - config, - body.invokeVirtualMethod( - OBJ_TO_STRING_METHOD, - name), - body.loadClass(Float.class), loadConverterClass(body)), Float.class); - final BranchResult ifNull = body.ifNull(floatValue); - final BytecodeCreator isNull = ifNull.trueBranch(); - isNull.assign(result, isNull.load(0f)); - final BytecodeCreator isNotNull = ifNull.falseBranch(); - isNotNull.assign(result, - isNotNull.invokeVirtualMethod( - FLOAT_VALUE_METHOD, - floatValue)); - body.invokeStaticMethod(setter, enclosing, result); - } - - public String getDefaultValueString() { - return defaultValue; - } - - @Override - public Class getItemClass() { - return float.class; - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - final Float value = ConfigUtils.convert(config, ExpandingConfigSource.expandValue(defaultValue, cache), Float.class, - converterClass); - field.setFloat(enclosing, value != null ? value.floatValue() : 0f); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - body.invokeStaticMethod(setter, enclosing, - body.invokeVirtualMethod(FLOAT_VALUE_METHOD, getConvertedDefault(body, cache, config))); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - return body.invokeVirtualMethod(FLOAT_VALUE_METHOD, getConvertedDefault(body, cache, smallRyeConfig)); - } - - private ResultHandle getConvertedDefault(final BytecodeCreator body, final ResultHandle cache, final ResultHandle config) { - return body.invokeStaticMethod( - CU_CONVERT, - config, - cache == null ? body.load(defaultValue) - : body.invokeStaticMethod( - ConfigurationSetup.ECS_EXPAND_VALUE, - body.load(defaultValue), - cache), - body.loadClass(Float.class), loadConverterClass(body)); - } - - @Override - public Class> getConverterClass() { - return converterClass; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/GroupConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/GroupConfigType.java deleted file mode 100644 index a26c35c3323c5..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/GroupConfigType.java +++ /dev/null @@ -1,300 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.lang.reflect.UndeclaredThrowableException; -import java.util.HashMap; -import java.util.Map; -import java.util.TreeSet; - -import org.wildfly.common.Assert; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - * A configuration definition node describing a configuration group. - */ -public class GroupConfigType extends CompoundConfigType { - - private final Map fields; - private final Class class_; - private final Constructor constructor; - private final MethodDescriptor constructorAccessor; - private final Map fieldInfos; - - public GroupConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final Class class_, final AccessorFinder accessorFinder) { - super(containingName, container, consumeSegment); - Assert.checkNotNullParam("containingName", containingName); - Assert.checkNotNullParam("container", container); - Assert.checkNotNullParam("class_", class_); - Assert.checkNotNullParam("accessorFinder", accessorFinder); - fields = new HashMap<>(); - this.class_ = class_; - try { - constructor = class_.getDeclaredConstructor(); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("Constructor of " + class_ + " is missing"); - } - if ((constructor.getModifiers() & Modifier.PRIVATE) != 0) { - throw new IllegalArgumentException("Constructor of " + class_ + " must not be private"); - } else if ((constructor.getModifiers() & Modifier.PUBLIC) == 0) { - constructor.setAccessible(true); - } - constructorAccessor = accessorFinder.getConstructorFor(MethodDescriptor.ofConstructor(class_)); - fieldInfos = new HashMap<>(); - for (Field field : class_.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if ((modifiers & Modifier.STATIC) == 0) { - // consider this one - if ((modifiers & Modifier.PRIVATE) != 0) { - throw new IllegalArgumentException( - "Field \"" + field.getName() + "\" of " + class_ + " must not be private"); - } - field.setAccessible(true); - final FieldDescriptor descr = FieldDescriptor.of(field); - fieldInfos.put(field.getName(), - new FieldInfo(field, accessorFinder.getSetterFor(descr), accessorFinder.getGetterFor(descr))); - } - } - } - - public void load() throws ClassNotFoundException { - assert class_ != null && constructor != null; - if (!fieldInfos.keySet().containsAll(fields.keySet())) { - final TreeSet missing = new TreeSet<>(fields.keySet()); - missing.removeAll(fieldInfos.keySet()); - throw new IllegalArgumentException("Fields missing from " + class_ + ": " + missing); - } - if (!fields.keySet().containsAll(fieldInfos.keySet())) { - final TreeSet extra = new TreeSet<>(fieldInfos.keySet()); - extra.removeAll(fields.keySet()); - throw new IllegalArgumentException("Extra unknown fields on " + class_ + ": " + extra); - } - for (ConfigType node : fields.values()) { - node.load(); - } - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - final ResultHandle instance = body - .invokeStaticMethod(accessorFinder.getConstructorFor(MethodDescriptor.ofConstructor(class_))); - for (Map.Entry entry : fields.entrySet()) { - final String fieldName = entry.getKey(); - final ConfigType fieldType = entry.getValue(); - final FieldDescriptor fieldDescriptor = FieldDescriptor.of(fieldInfos.get(fieldName).getField()); - final ResultHandle value = fieldType.writeInitialization(body, accessorFinder, cache, smallRyeConfig); - body.invokeStaticMethod(accessorFinder.getSetterFor(fieldDescriptor), instance, value); - } - return instance; - } - - public ConfigType getField(String name) { - return fields.get(name); - } - - public void addField(ConfigType node) { - final String containingName = node.getContainingName(); - final ConfigType existing = fields.putIfAbsent(containingName, node); - if (existing != null) { - throw new IllegalArgumentException("Cannot add duplicate field \"" + containingName + "\" to " + this); - } - } - - private Field findField(final String name) { - if (class_ == null) - throw notLoadedException(); - final FieldInfo fieldInfo = fieldInfos.get(name); - if (fieldInfo == null) - throw new IllegalStateException("Missing field " + name + " on " + class_); - return fieldInfo.getField(); - } - - private Object create(final ExpandingConfigSource.Cache cache, final SmallRyeConfig config) { - Object self; - try { - self = constructor.newInstance(); - } catch (InstantiationException e) { - throw toError(e); - } catch (IllegalAccessException e) { - throw toError(e); - } catch (InvocationTargetException e) { - try { - throw e.getCause(); - } catch (RuntimeException | Error e2) { - throw e2; - } catch (Throwable t) { - throw new UndeclaredThrowableException(t); - } - } - for (Map.Entry entry : fields.entrySet()) { - entry.getValue().getDefaultValueIntoEnclosingGroup(self, cache, config, findField(entry.getKey())); - } - return self; - } - - private ResultHandle generateCreate(final BytecodeCreator body, final ResultHandle cache, final ResultHandle config) { - final ResultHandle self = body.invokeStaticMethod(constructorAccessor); - for (Map.Entry entry : fields.entrySet()) { - final ConfigType childType = entry.getValue(); - final MethodDescriptor setter = fieldInfos.get(entry.getKey()).getSetter(); - childType.generateGetDefaultValueIntoEnclosingGroup(body, self, setter, cache, config); - } - return self; - } - - Object getChildObject(final NameIterator name, final ExpandingConfigSource.Cache cache, final SmallRyeConfig config, - final Object self, final String childName) { - final Field field = findField(childName); - Object val = getFromField(field, self); - if (val == null) { - final ConfigType childType = getField(childName); - childType.getDefaultValueIntoEnclosingGroup(self, cache, config, field); - val = getFromField(field, self); - } - return val; - } - - ResultHandle generateGetChildObject(final BytecodeCreator body, final ResultHandle name, final ResultHandle cache, - final ResultHandle config, - final ResultHandle self, final String childName) { - final AssignableResultHandle val = body.createVariable(Object.class); - final FieldInfo fieldInfo = fieldInfos.get(childName); - body.assign(val, body.invokeStaticMethod(fieldInfo.getGetter(), self)); - try (BytecodeCreator isNull = body.ifNull(val).trueBranch()) { - final ConfigType childType = getField(childName); - childType.generateGetDefaultValueIntoEnclosingGroup(isNull, self, fieldInfo.getSetter(), cache, config); - isNull.assign(val, isNull.invokeStaticMethod(fieldInfo.getGetter(), self)); - } - return val; - } - - private static Object getFromField(Field field, Object obj) { - try { - return field.get(obj); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - Object getOrCreate(final NameIterator name, final ExpandingConfigSource.Cache cache, final SmallRyeConfig config) { - final CompoundConfigType container = getContainer(); - if (isConsumeSegment()) - name.previous(); - final Object enclosing = container.getOrCreate(name, cache, config); - Object self = container.getChildObject(name, cache, config, enclosing, getContainingName()); - if (isConsumeSegment()) - name.next(); - if (self == null) { - // it's a map, and it doesn't contain our key. - self = create(cache, config); - if (isConsumeSegment()) - name.previous(); - container.setChildObject(name, enclosing, getContainingName(), self); - if (isConsumeSegment()) - name.next(); - } - return self; - } - - ResultHandle generateGetOrCreate(final BytecodeCreator body, final ResultHandle name, final ResultHandle cache, - final ResultHandle config) { - final CompoundConfigType container = getContainer(); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - final ResultHandle enclosing = container.generateGetOrCreate(body, name, cache, config); - final AssignableResultHandle var = body.createVariable(Object.class); - body.assign(var, container.generateGetChildObject(body, name, cache, config, enclosing, getContainingName())); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_NEXT_METHOD, name); - if (container.getClass() == MapConfigType.class) { - // it could be null - try (BytecodeCreator createBranch = body.ifNull(var).trueBranch()) { - createBranch.assign(var, generateCreate(createBranch, cache, config)); - if (isConsumeSegment()) - createBranch.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateSetChildObject(createBranch, name, enclosing, getContainingName(), var); - if (isConsumeSegment()) - createBranch.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - } - return var; - } - - void acceptConfigurationValueIntoLeaf(final LeafConfigType leafType, final NameIterator name, - final ExpandingConfigSource.Cache cache, final SmallRyeConfig config) { - final FieldInfo fieldInfo = fieldInfos.get(leafType.getContainingName()); - leafType.acceptConfigurationValueIntoGroup(getOrCreate(name, cache, config), fieldInfo.getField(), name, config); - } - - void generateAcceptConfigurationValueIntoLeaf(final BytecodeCreator body, final LeafConfigType leafType, - final ResultHandle name, final ResultHandle cache, final ResultHandle config) { - final FieldInfo fieldInfo = fieldInfos.get(leafType.getContainingName()); - leafType.generateAcceptConfigurationValueIntoGroup(body, generateGetOrCreate(body, name, cache, config), - fieldInfo.getSetter(), - name, config); - } - - void setChildObject(final NameIterator name, final Object self, final String containingName, final Object value) { - try { - findField(containingName).set(self, value); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateSetChildObject(final BytecodeCreator body, final ResultHandle name, final ResultHandle self, - final String containingName, final ResultHandle value) { - body.invokeStaticMethod(fieldInfos.get(containingName).getSetter(), self, value); - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - field.set(enclosing, create(cache, config)); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - final ResultHandle self = generateCreate(body, cache, config); - body.invokeStaticMethod(setter, enclosing, self); - } - - static final class FieldInfo { - private final Field field; - private final MethodDescriptor setter; - private final MethodDescriptor getter; - - public FieldInfo(final Field field, final MethodDescriptor setter, final MethodDescriptor getter) { - this.field = field; - this.setter = setter; - this.getter = getter; - } - - public Field getField() { - return field; - } - - public MethodDescriptor getSetter() { - return setter; - } - - public MethodDescriptor getGetter() { - return getter; - } - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/IntConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/IntConfigType.java deleted file mode 100644 index f4e85da179f40..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/IntConfigType.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; - -import org.eclipse.microprofile.config.spi.Converter; -import org.wildfly.common.Assert; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.deployment.steps.ConfigurationSetup; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class IntConfigType extends LeafConfigType { - private static final MethodDescriptor INT_VALUE_METHOD = MethodDescriptor.ofMethod(Integer.class, "intValue", int.class); - - final String defaultValue; - private final Class> converterClass; - - public IntConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final String defaultValue, String javadocKey, String configKey, - Class> converterClass) { - super(containingName, container, consumeSegment, javadocKey, configKey); - Assert.checkNotEmptyParam("defaultValue", defaultValue); - this.defaultValue = defaultValue; - this.converterClass = converterClass; - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - name.previous(); - container.acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - public void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - Integer value = ConfigUtils.getValue(config, name.toString(), Integer.class, converterClass); - field.setInt(enclosing, value != null ? value.intValue() : 0); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - public void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - // final Integer integerValue = ConfigUtils.getValue(config, name.toString(), Integer.class, converterClass); - // final int i = integerValue != null ? integerValue.intValue() : 0; - final AssignableResultHandle result = body.createVariable(int.class); - final ResultHandle integerValue = body.checkCast(body.invokeStaticMethod( - CU_GET_VALUE, - config, - body.invokeVirtualMethod( - OBJ_TO_STRING_METHOD, - name), - body.loadClass(Integer.class), loadConverterClass(body)), Integer.class); - final BranchResult ifNull = body.ifNull(integerValue); - final BytecodeCreator isNull = ifNull.trueBranch(); - isNull.assign(result, isNull.load(0)); - final BytecodeCreator isNotNull = ifNull.falseBranch(); - isNotNull.assign(result, - isNotNull.invokeVirtualMethod( - INT_VALUE_METHOD, - integerValue)); - body.invokeStaticMethod(setter, enclosing, result); - } - - public String getDefaultValueString() { - return defaultValue; - } - - @Override - public Class getItemClass() { - return int.class; - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - Integer value = ConfigUtils.convert(config, ExpandingConfigSource.expandValue(defaultValue, cache), - Integer.class, converterClass); - field.setInt(enclosing, value != null ? value.intValue() : 0); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - body.invokeStaticMethod(setter, enclosing, - body.invokeVirtualMethod(INT_VALUE_METHOD, getConvertedDefault(body, cache, config))); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - return body.invokeVirtualMethod(INT_VALUE_METHOD, getConvertedDefault(body, cache, smallRyeConfig)); - } - - private ResultHandle getConvertedDefault(final BytecodeCreator body, final ResultHandle cache, final ResultHandle config) { - return body.invokeStaticMethod( - CU_CONVERT, - config, - cache == null ? body.load(defaultValue) - : body.invokeStaticMethod( - ConfigurationSetup.ECS_EXPAND_VALUE, - body.load(defaultValue), - cache), - body.loadClass(Integer.class), loadConverterClass(body)); - } - - @Override - public Class> getConverterClass() { - return converterClass; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/LeafConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/LeafConfigType.java deleted file mode 100644 index 4ff48cdcc5ac6..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/LeafConfigType.java +++ /dev/null @@ -1,102 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Optional; - -import org.eclipse.microprofile.config.spi.Converter; -import org.wildfly.common.Assert; -import org.wildfly.common.annotation.NotNull; - -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - * A node which contains a regular value. Leaf nodes can never be directly acquired. - */ -public abstract class LeafConfigType extends ConfigType { - static final MethodDescriptor CU_CONVERT = MethodDescriptor.ofMethod(ConfigUtils.class, "convert", Object.class, - SmallRyeConfig.class, String.class, Class.class, Class.class); - static final MethodDescriptor CU_GET_VALUE = MethodDescriptor.ofMethod(ConfigUtils.class, "getValue", Object.class, - SmallRyeConfig.class, String.class, Class.class, Class.class); - static final MethodDescriptor CU_GET_OPT_VALUE = MethodDescriptor.ofMethod(ConfigUtils.class, "getOptionalValue", - Optional.class, SmallRyeConfig.class, String.class, Class.class, Class.class); - - private final String javadocKey; - private final String configKey; - - LeafConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - String javadocKey, String configKey) { - super(containingName, container, consumeSegment); - this.javadocKey = javadocKey; - this.configKey = configKey; - } - - /** - * - * @return the key that the javadoc was saved under - */ - public String getJavadocKey() { - return javadocKey; - } - - public String getConfigKey() { - return configKey; - } - - public void load() { - } - - /** - * Get the class of the individual item. This is the unwrapped type of {@code Optional}, {@code Collection}, etc. - * - * @return the item class (must not be {@code null}) - */ - public abstract Class getItemClass(); - - /** - * Handle a configuration key from the input file. - * - * @param name the configuration property name - * @param cache - * @param config the source configuration - */ - public abstract void acceptConfigurationValue(@NotNull NameIterator name, final ExpandingConfigSource.Cache cache, - @NotNull SmallRyeConfig config); - - public abstract void generateAcceptConfigurationValue(BytecodeCreator body, ResultHandle name, final ResultHandle cache, - ResultHandle config); - - abstract void acceptConfigurationValueIntoGroup(Object enclosing, Field field, NameIterator name, SmallRyeConfig config); - - abstract void generateAcceptConfigurationValueIntoGroup(BytecodeCreator body, ResultHandle enclosing, - final MethodDescriptor setter, ResultHandle name, ResultHandle config); - - void acceptConfigurationValueIntoMap(Map enclosing, NameIterator name, SmallRyeConfig config) { - // only non-primitives are supported - throw Assert.unsupported(); - } - - void generateAcceptConfigurationValueIntoMap(BytecodeCreator body, ResultHandle enclosing, - ResultHandle name, ResultHandle config) { - throw Assert.unsupported(); - } - - public abstract String getDefaultValueString(); - - public abstract Class> getConverterClass(); - - protected final ResultHandle loadConverterClass(BytecodeCreator body) { - Class> converterClass = getConverterClass(); - ResultHandle converter = body.loadNull(); - if (converterClass != null) { - converter = body.loadClass(converterClass); - } - return converter; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/LongConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/LongConfigType.java deleted file mode 100644 index 1ca42e42a4c75..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/LongConfigType.java +++ /dev/null @@ -1,136 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; - -import org.eclipse.microprofile.config.spi.Converter; -import org.wildfly.common.Assert; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.deployment.steps.ConfigurationSetup; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class LongConfigType extends LeafConfigType { - private static final MethodDescriptor LONG_VALUE_METHOD = MethodDescriptor.ofMethod(Long.class, "longValue", long.class); - - final String defaultValue; - private final Class> converterClass; - - public LongConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final String defaultValue, String javadocKey, String configKey, Class> converterClass) { - super(containingName, container, consumeSegment, javadocKey, configKey); - Assert.checkNotEmptyParam("defaultValue", defaultValue); - this.defaultValue = defaultValue; - this.converterClass = converterClass; - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - name.previous(); - container.acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - final GroupConfigType container = getContainer(GroupConfigType.class); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - public void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - Long value = ConfigUtils.getValue(config, name.toString(), Long.class, converterClass); - field.setLong(enclosing, value != null ? value.longValue() : 0L); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - public void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - // final Long longValue = ConfigUtils.getValue(config, name.toString(), Long.class, converterClass); - // final long l = longValue != null ? longValue.longValue() : 0l; - final AssignableResultHandle result = body.createVariable(long.class); - final ResultHandle longValue = body.checkCast(body.invokeStaticMethod( - CU_GET_VALUE, - config, - body.invokeVirtualMethod( - OBJ_TO_STRING_METHOD, - name), - body.loadClass(Long.class), loadConverterClass(body)), Long.class); - final BranchResult ifNull = body.ifNull(longValue); - final BytecodeCreator isNull = ifNull.trueBranch(); - isNull.assign(result, isNull.load(0L)); - final BytecodeCreator isNotNull = ifNull.falseBranch(); - isNotNull.assign(result, - isNotNull.invokeVirtualMethod( - LONG_VALUE_METHOD, - longValue)); - body.invokeStaticMethod(setter, enclosing, result); - } - - public String getDefaultValueString() { - return defaultValue; - } - - @Override - public Class getItemClass() { - return long.class; - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - Long value = ConfigUtils.convert(config, ExpandingConfigSource.expandValue(defaultValue, cache), Long.class, - converterClass); - field.setLong(enclosing, value != null ? value.longValue() : 0L); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - body.invokeStaticMethod(setter, enclosing, - body.invokeVirtualMethod(LONG_VALUE_METHOD, getConvertedDefault(body, cache, config))); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - return body.invokeVirtualMethod(LONG_VALUE_METHOD, getConvertedDefault(body, cache, smallRyeConfig)); - } - - private ResultHandle getConvertedDefault(final BytecodeCreator body, final ResultHandle cache, final ResultHandle config) { - return body.invokeStaticMethod( - CU_CONVERT, - config, - cache == null ? body.load(defaultValue) - : body.invokeStaticMethod( - ConfigurationSetup.ECS_EXPAND_VALUE, - body.load(defaultValue), - cache), - body.loadClass(Long.class), loadConverterClass(body)); - } - - @Override - public Class> getConverterClass() { - return converterClass; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/MapConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/MapConfigType.java deleted file mode 100644 index a2ce0e360c52a..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/MapConfigType.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.TreeMap; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class MapConfigType extends CompoundConfigType { - - private static final MethodDescriptor TREE_MAP_CTOR = MethodDescriptor.ofConstructor(TreeMap.class); - private static final MethodDescriptor MAP_GET_METHOD = MethodDescriptor.ofMethod(Map.class, "get", Object.class, - Object.class); - private static final MethodDescriptor MAP_PUT_METHOD = MethodDescriptor.ofMethod(Map.class, "put", Object.class, - Object.class, Object.class); - - public MapConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment) { - super(containingName, container, consumeSegment); - } - - public void load() { - } - - @SuppressWarnings("unchecked") - Object getChildObject(final NameIterator name, final ExpandingConfigSource.Cache cache, final SmallRyeConfig config, - final Object self, final String childName) { - return ((TreeMap) self).get(name.getNextSegment()); - } - - ResultHandle generateGetChildObject(final BytecodeCreator body, final ResultHandle name, final ResultHandle cache, - final ResultHandle config, - final ResultHandle self, final String childName) { - return body.invokeInterfaceMethod(MAP_GET_METHOD, body.checkCast(self, Map.class), - body.invokeVirtualMethod(NI_GET_NEXT_SEGMENT, name)); - } - - @SuppressWarnings("unchecked") - void setChildObject(final NameIterator name, final Object self, final String childName, final Object value) { - ((TreeMap) self).put(name.getNextSegment(), value); - } - - void generateSetChildObject(final BytecodeCreator body, final ResultHandle name, final ResultHandle self, - final String containingName, final ResultHandle value) { - body.invokeInterfaceMethod(MAP_PUT_METHOD, body.checkCast(self, Map.class), - body.invokeVirtualMethod(NI_GET_NEXT_SEGMENT, name), value); - } - - TreeMap getOrCreate(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final CompoundConfigType container = getContainer(); - TreeMap self; - if (container != null) { - if (isConsumeSegment()) - name.previous(); - final Object enclosing = container.getOrCreate(name, cache, config); - self = (TreeMap) container.getChildObject(name, cache, config, enclosing, getContainingName()); - if (self == null) { - self = new TreeMap<>(); - container.setChildObject(name, enclosing, getContainingName(), self); - } - if (isConsumeSegment()) - name.next(); - } else { - self = new TreeMap<>(); - } - return self; - } - - ResultHandle generateGetOrCreate(final BytecodeCreator body, final ResultHandle name, final ResultHandle cache, - final ResultHandle config) { - final CompoundConfigType container = getContainer(); - if (container != null) { - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - final ResultHandle enclosing = container.generateGetOrCreate(body, name, cache, config); - final AssignableResultHandle self = body.createVariable(TreeMap.class); - body.assign(self, body.checkCast( - container.generateGetChildObject(body, name, cache, config, enclosing, getContainingName()), Map.class)); - try (BytecodeCreator selfIsNull = body.ifNull(self).trueBranch()) { - selfIsNull.assign(self, selfIsNull.newInstance(TREE_MAP_CTOR)); - container.generateSetChildObject(selfIsNull, name, enclosing, getContainingName(), self); - } - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_NEXT_METHOD, name); - return self; - } else { - return body.newInstance(TREE_MAP_CTOR); - } - } - - void acceptConfigurationValueIntoLeaf(final LeafConfigType leafType, final NameIterator name, - final ExpandingConfigSource.Cache cache, final SmallRyeConfig config) { - leafType.acceptConfigurationValueIntoMap(getOrCreate(name, cache, config), name, config); - } - - void generateAcceptConfigurationValueIntoLeaf(final BytecodeCreator body, final LeafConfigType leafType, - final ResultHandle name, final ResultHandle cache, final ResultHandle config) { - leafType.generateAcceptConfigurationValueIntoMap(body, generateGetOrCreate(body, name, cache, config), name, config); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - return body.newInstance(TREE_MAP_CTOR); - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - field.set(enclosing, new TreeMap<>()); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - body.invokeStaticMethod(setter, enclosing, body.newInstance(TREE_MAP_CTOR)); - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ObjectConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ObjectConfigType.java deleted file mode 100644 index 90a3c95de3146..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ObjectConfigType.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.quarkus.deployment.configuration; - -import static io.quarkus.deployment.steps.ConfigurationSetup.ECS_EXPAND_VALUE; - -import java.lang.reflect.Field; -import java.util.Map; - -import org.eclipse.microprofile.config.spi.Converter; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class ObjectConfigType extends LeafConfigType { - final String defaultValue; - final Class expectedType; - Class> converterClass; - - public ObjectConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final String defaultValue, final Class expectedType, String javadocKey, String configKey, - Class> converterClass) { - super(containingName, container, consumeSegment, javadocKey, configKey); - this.defaultValue = defaultValue; - this.expectedType = expectedType; - this.converterClass = converterClass; - } - - @Override - public Class getItemClass() { - return expectedType; - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - String value = ExpandingConfigSource.expandValue(defaultValue, cache); - field.set(enclosing, ConfigUtils.convert(config, value, expectedType, converterClass)); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - ResultHandle resultHandle = getResultHandle(body, cache, config); - body.invokeStaticMethod(setter, enclosing, resultHandle); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle smallRyeConfig) { - ResultHandle resultHandle = getResultHandle(body, cache, smallRyeConfig); - return body.checkCast(resultHandle, expectedType); - } - - private ResultHandle getResultHandle(BytecodeCreator body, ResultHandle cache, ResultHandle smallRyeConfig) { - ResultHandle clazz = body.loadClass(expectedType); - ResultHandle cacheResultHandle = cache == null ? body.load(defaultValue) - : body.invokeStaticMethod(ECS_EXPAND_VALUE, - body.load(defaultValue), - cache); - - return body.invokeStaticMethod(CU_CONVERT, smallRyeConfig, cacheResultHandle, clazz, loadConverterClass(body)); - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - if (isConsumeSegment()) - name.previous(); - getContainer().acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - getContainer().generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - field.set(enclosing, - ConfigUtils.getOptionalValue(config, name.toString(), expectedType, converterClass) - .orElse(null)); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - body.invokeStaticMethod(setter, enclosing, generateGetValue(body, name, config)); - } - - void acceptConfigurationValueIntoMap(final Map enclosing, final NameIterator name, - final SmallRyeConfig config) { - enclosing.put(name.getNextSegment(), - ConfigUtils.getOptionalValue(config, name.toString(), expectedType, converterClass).orElse(null)); - } - - void generateAcceptConfigurationValueIntoMap(final BytecodeCreator body, final ResultHandle enclosing, - final ResultHandle name, final ResultHandle config) { - body.invokeInterfaceMethod(MAP_PUT_METHOD, enclosing, body.invokeVirtualMethod(NI_GET_NEXT_SEGMENT, name), - generateGetValue(body, name, config)); - } - - public String getDefaultValueString() { - return defaultValue; - } - - private ResultHandle generateGetValue(final BytecodeCreator body, final ResultHandle name, final ResultHandle config) { - final ResultHandle optionalValue = body.invokeStaticMethod( - CU_GET_OPT_VALUE, - config, - body.invokeVirtualMethod( - OBJ_TO_STRING_METHOD, - name), - body.loadClass(expectedType), loadConverterClass(body)); - return body.invokeVirtualMethod(OPT_OR_ELSE_METHOD, optionalValue, body.loadNull()); - } - - @Override - public Class> getConverterClass() { - return converterClass; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ObjectListConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ObjectListConfigType.java deleted file mode 100644 index d956fe8ece863..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ObjectListConfigType.java +++ /dev/null @@ -1,138 +0,0 @@ -package io.quarkus.deployment.configuration; - -import static io.quarkus.deployment.steps.ConfigurationSetup.ECS_EXPAND_VALUE; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.IntFunction; - -import org.eclipse.microprofile.config.spi.Converter; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ArrayListFactory; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class ObjectListConfigType extends ObjectConfigType { - static final MethodDescriptor ALF_GET_INST_METHOD = MethodDescriptor.ofMethod(ArrayListFactory.class, "getInstance", - ArrayListFactory.class); - static final MethodDescriptor EMPTY_LIST_METHOD = MethodDescriptor.ofMethod(Collections.class, "emptyList", List.class); - static final MethodDescriptor CU_GET_DEFAULTS_METHOD = MethodDescriptor.ofMethod(ConfigUtils.class, "getDefaults", - Collection.class, SmallRyeConfig.class, String.class, Class.class, Class.class, IntFunction.class); - - static final MethodDescriptor GET_VALUES = MethodDescriptor.ofMethod(ConfigUtils.class, "getValues", ArrayList.class, - SmallRyeConfig.class, String.class, Class.class, Class.class); - - public ObjectListConfigType(final String containingName, final CompoundConfigType container, final boolean consumeSegment, - final String defaultValue, final Class expectedType, String javadocKey, String configKey, - Class> converterClass) { - super(containingName, container, consumeSegment, defaultValue, expectedType, javadocKey, configKey, converterClass); - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final CompoundConfigType container = getContainer(); - if (isConsumeSegment()) - name.previous(); - container.acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - final CompoundConfigType container = getContainer(); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - if (defaultValue.isEmpty()) { - field.set(enclosing, Collections.emptyList()); - } else { - final ArrayList defaults = ConfigUtils.getDefaults( - config, - ExpandingConfigSource.expandValue(defaultValue, cache), - expectedType, - converterClass, - ArrayListFactory.getInstance()); - field.set(enclosing, defaults); - } - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - final ResultHandle value; - if (defaultValue.isEmpty()) { - value = body.invokeStaticMethod(EMPTY_LIST_METHOD); - } else { - ResultHandle cacheValue = cache == null ? body.load(defaultValue) - : body.invokeStaticMethod(ECS_EXPAND_VALUE, - body.load(defaultValue), - cache); - value = body.invokeStaticMethod(CU_GET_DEFAULTS_METHOD, config, cacheValue, body.loadClass(expectedType), - loadConverterClass(body), - body.invokeStaticMethod(ALF_GET_INST_METHOD)); - } - body.invokeStaticMethod(setter, enclosing, value); - } - - public void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - field.set(enclosing, ConfigUtils.getValues(config, name.toString(), expectedType, converterClass)); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - public void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - body.invokeStaticMethod(setter, enclosing, generateGetValues(body, name, config)); - } - - void acceptConfigurationValueIntoMap(final Map enclosing, final NameIterator name, - final SmallRyeConfig config) { - enclosing.put(name.getNextSegment(), - ConfigUtils.getValues(config, name.toString(), expectedType, converterClass)); - } - - void generateAcceptConfigurationValueIntoMap(final BytecodeCreator body, final ResultHandle enclosing, - final ResultHandle name, final ResultHandle config) { - body.invokeInterfaceMethod(MAP_PUT_METHOD, enclosing, body.invokeVirtualMethod(NI_GET_NEXT_SEGMENT, name), - generateGetValues(body, name, config)); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle config) { - ResultHandle arrayListFactory = body.invokeStaticMethod(ALF_GET_INST_METHOD); - final ResultHandle resultHandle = body.invokeStaticMethod(CU_GET_DEFAULTS_METHOD, config, body.load(defaultValue), - body.loadClass(expectedType), loadConverterClass(body), arrayListFactory); - return body.checkCast(resultHandle, List.class); - } - - private ResultHandle generateGetValues(final BytecodeCreator body, final ResultHandle name, final ResultHandle config) { - ResultHandle propertyName = body.invokeVirtualMethod(OBJ_TO_STRING_METHOD, name); - return body.invokeStaticMethod(GET_VALUES, config, propertyName, body.loadClass(expectedType), - loadConverterClass(body)); - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/OptionalObjectConfigType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/OptionalObjectConfigType.java deleted file mode 100644 index 385269fe68a28..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/OptionalObjectConfigType.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.quarkus.deployment.configuration; - -import static io.quarkus.deployment.steps.ConfigurationSetup.ECS_EXPAND_VALUE; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Optional; - -import org.eclipse.microprofile.config.spi.Converter; -import org.wildfly.common.Assert; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.NameIterator; -import io.smallrye.config.SmallRyeConfig; - -/** - */ -public class OptionalObjectConfigType extends ObjectConfigType { - - public OptionalObjectConfigType(final String containingName, final CompoundConfigType container, - final boolean consumeSegment, final String defaultValue, final Class expectedType, String javadocKey, - String configKey, Class> converterClass) { - super(containingName, container, consumeSegment, defaultValue, expectedType, javadocKey, configKey, converterClass); - } - - public void acceptConfigurationValue(final NameIterator name, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config) { - final CompoundConfigType container = getContainer(); - if (isConsumeSegment()) - name.previous(); - container.acceptConfigurationValueIntoLeaf(this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) name.next(); - } - - public void generateAcceptConfigurationValue(final BytecodeCreator body, final ResultHandle name, - final ResultHandle cache, final ResultHandle config) { - final CompoundConfigType container = getContainer(); - if (isConsumeSegment()) - body.invokeVirtualMethod(NI_PREV_METHOD, name); - container.generateAcceptConfigurationValueIntoLeaf(body, this, name, cache, config); - // the iterator is not used after this point - // if (isConsumeSegment()) body.invokeVirtualMethod(NI_NEXT_METHOD, name); - } - - void getDefaultValueIntoEnclosingGroup(final Object enclosing, final ExpandingConfigSource.Cache cache, - final SmallRyeConfig config, final Field field) { - try { - if (defaultValue.isEmpty()) { - field.set(enclosing, Optional.empty()); - } else { - String value = ExpandingConfigSource.expandValue(defaultValue, cache); - field.set(enclosing, - Optional.ofNullable(ConfigUtils.convert(config, value, expectedType, converterClass))); - } - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - void generateGetDefaultValueIntoEnclosingGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle cache, final ResultHandle config) { - final ResultHandle optValue; - if (defaultValue.isEmpty()) { - optValue = body.invokeStaticMethod(OPT_EMPTY_METHOD); - } else { - optValue = body.invokeStaticMethod(OPT_OF_NULLABLE_METHOD, body.invokeStaticMethod(CU_CONVERT, config, - body.load(defaultValue), body.loadClass(expectedType), loadConverterClass(body))); - } - body.invokeStaticMethod(setter, enclosing, optValue); - } - - public void acceptConfigurationValueIntoGroup(final Object enclosing, final Field field, final NameIterator name, - final SmallRyeConfig config) { - try { - field.set(enclosing, ConfigUtils.getOptionalValue(config, name.toString(), expectedType, converterClass)); - } catch (IllegalAccessException e) { - throw toError(e); - } - } - - public void generateAcceptConfigurationValueIntoGroup(final BytecodeCreator body, final ResultHandle enclosing, - final MethodDescriptor setter, final ResultHandle name, final ResultHandle config) { - ResultHandle propertyName = body.invokeVirtualMethod(OBJ_TO_STRING_METHOD, name); - final ResultHandle optionalValue = body.invokeStaticMethod(CU_GET_OPT_VALUE, config, propertyName, - body.loadClass(expectedType), loadConverterClass(body)); - body.invokeStaticMethod(setter, enclosing, optionalValue); - } - - void acceptConfigurationValueIntoMap(final Map enclosing, final NameIterator name, - final SmallRyeConfig config) { - throw Assert.unsupported(); - } - - void generateAcceptConfigurationValueIntoMap(final BytecodeCreator body, final ResultHandle enclosing, - final ResultHandle name, final ResultHandle config) { - throw Assert.unsupported(); - } - - public ResultHandle writeInitialization(final BytecodeCreator body, final AccessorFinder accessorFinder, - final ResultHandle cache, final ResultHandle config) { - if (defaultValue.isEmpty()) { - return body.invokeStaticMethod(OPT_EMPTY_METHOD); - } else { - ResultHandle classResultHandle = body.loadClass(expectedType); - ResultHandle cacheResultHandle = cache == null ? body.load(defaultValue) - : body.invokeStaticMethod(ECS_EXPAND_VALUE, - body.load(defaultValue), - cache); - ResultHandle resultHandle = body.invokeStaticMethod(CU_CONVERT, config, cacheResultHandle, classResultHandle, - loadConverterClass(body)); - return body.invokeStaticMethod(OPT_OF_NULLABLE_METHOD, resultHandle); - } - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/PropertiesUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/PropertiesUtil.java index 4b20a9f154444..c8eb215cdb1f6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/PropertiesUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/PropertiesUtil.java @@ -4,20 +4,20 @@ public class PropertiesUtil { private PropertiesUtil() { } - public static boolean escape(int codePoint) { + public static boolean needsEscape(int codePoint) { return codePoint == '#' || codePoint == '!' || codePoint == '=' || codePoint == ':'; } - public static boolean escapeForKey(int codePoint) { - return Character.isSpaceChar(codePoint) || escape(codePoint); + public static boolean needsEscapeForKey(int codePoint) { + return Character.isSpaceChar(codePoint) || needsEscape(codePoint); } - public static boolean escapeForValueFirst(int codePoint) { - return escapeForKey(codePoint); + public static boolean needsEscapeForValueFirst(int codePoint) { + return needsEscapeForKey(codePoint); } - public static boolean escapeForValueSubsequent(int codePoint) { - return escape(codePoint); + public static boolean needsEscapeForValueSubsequent(int codePoint) { + return needsEscape(codePoint); } public static String quotePropertyName(String name) { @@ -25,7 +25,7 @@ public static String quotePropertyName(String name) { int cp; for (int i = 0; i < length; i = name.offsetByCodePoints(i, 1)) { cp = name.codePointAt(i); - if (escapeForKey(cp)) { + if (needsEscapeForKey(cp)) { final StringBuilder b = new StringBuilder(length + (length >> 2)); // get leading section b.append(name, 0, i); @@ -33,7 +33,7 @@ public static String quotePropertyName(String name) { b.append('\\').appendCodePoint(cp); for (i = name.offsetByCodePoints(i, 1); i < length; i = name.offsetByCodePoints(i, 1)) { cp = name.codePointAt(i); - if (escapeForKey(cp)) { + if (needsEscapeForKey(cp)) { b.append('\\'); } b.appendCodePoint(cp); @@ -50,7 +50,7 @@ public static String quotePropertyValue(String value) { int cp; for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { cp = value.codePointAt(i); - if (i == 0 ? escapeForValueFirst(cp) : escapeForValueSubsequent(cp)) { + if (i == 0 ? needsEscapeForValueFirst(cp) : needsEscapeForValueSubsequent(cp)) { final StringBuilder b = new StringBuilder(length + (length >> 2)); // get leading section b.append(value, 0, i); @@ -58,7 +58,7 @@ public static String quotePropertyValue(String value) { b.append('\\').appendCodePoint(cp); for (i = value.offsetByCodePoints(i, 1); i < length; i = value.offsetByCodePoints(i, 1)) { cp = value.codePointAt(i); - if (escapeForValueSubsequent(cp)) { + if (needsEscapeForValueSubsequent(cp)) { b.append('\\'); } b.appendCodePoint(cp); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java new file mode 100644 index 0000000000000..9400690846526 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -0,0 +1,1301 @@ +package io.quarkus.deployment.configuration; + +import static io.quarkus.deployment.util.ReflectUtil.reportError; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.function.IntFunction; +import java.util.regex.Pattern; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigBuilder; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.eclipse.microprofile.config.spi.Converter; +import org.objectweb.asm.Opcodes; +import org.wildfly.common.Assert; + +import io.quarkus.deployment.AccessorFinder; +import io.quarkus.deployment.configuration.definition.ClassDefinition; +import io.quarkus.deployment.configuration.definition.GroupDefinition; +import io.quarkus.deployment.configuration.definition.RootDefinition; +import io.quarkus.deployment.configuration.matching.ConfigPatternMap; +import io.quarkus.deployment.configuration.matching.Container; +import io.quarkus.deployment.configuration.matching.FieldContainer; +import io.quarkus.deployment.configuration.matching.MapContainer; +import io.quarkus.deployment.configuration.type.ArrayOf; +import io.quarkus.deployment.configuration.type.CollectionOf; +import io.quarkus.deployment.configuration.type.ConverterType; +import io.quarkus.deployment.configuration.type.Leaf; +import io.quarkus.deployment.configuration.type.LowerBoundCheckOf; +import io.quarkus.deployment.configuration.type.MinMaxValidated; +import io.quarkus.deployment.configuration.type.OptionalOf; +import io.quarkus.deployment.configuration.type.PatternValidated; +import io.quarkus.deployment.configuration.type.UpperBoundCheckOf; +import io.quarkus.gizmo.AssignableResultHandle; +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.CatchBlockCreator; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.gizmo.TryBlock; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.configuration.AbstractRawDefaultConfigSource; +import io.quarkus.runtime.configuration.ConfigDiagnostic; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.configuration.HyphenateEnumConverter; +import io.quarkus.runtime.configuration.NameIterator; +import io.quarkus.runtime.configuration.ProfileManager; +import io.quarkus.runtime.configuration.QuarkusConfigFactory; +import io.smallrye.config.Converters; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +/** + * + */ +public final class RunTimeConfigurationGenerator { + + public static final String CONFIG_CLASS_NAME = "io.quarkus.runtime.generated.Config"; + static final String RTDVCS_CLASS_NAME = "io.quarkus.runtime.generated.RunTimeDefaultValuesConfigSource"; + static final String BTRTDVCS_CLASS_NAME = "io.quarkus.runtime.generated.BuildTimeRunTimeDefaultValuesConfigSource"; + + // member descriptors + + static final MethodDescriptor BTRTDVCS_NEW = MethodDescriptor.ofConstructor(BTRTDVCS_CLASS_NAME); + + static final FieldDescriptor C_BUILD_TIME_CONFIG_SOURCE = FieldDescriptor.of(CONFIG_CLASS_NAME, "buildTimeConfigSource", + ConfigSource.class); + static final FieldDescriptor C_BUILD_TIME_RUN_TIME_DEFAULTS_CONFIG_SOURCE = FieldDescriptor.of(CONFIG_CLASS_NAME, + "buildTimeRunTimeDefaultsConfigSource", ConfigSource.class); + public static final MethodDescriptor C_CREATE_RUN_TIME_CONFIG = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, + "createRunTimeConfig", void.class); + public static final MethodDescriptor C_ENSURE_INITIALIZED = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, + "ensureInitialized", void.class); + static final FieldDescriptor C_RUN_TIME_DEFAULTS_CONFIG_SOURCE = FieldDescriptor.of(CONFIG_CLASS_NAME, + "runTimeDefaultsConfigSource", ConfigSource.class); + static final MethodDescriptor C_READ_CONFIG = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "readConfig", void.class); + static final FieldDescriptor C_SPECIFIED_RUN_TIME_CONFIG_SOURCE = FieldDescriptor.of(CONFIG_CLASS_NAME, + "specifiedRunTimeConfigSource", + ConfigSource.class); + + static final MethodDescriptor CD_INVALID_VALUE = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "invalidValue", + void.class, String.class, IllegalArgumentException.class); + static final MethodDescriptor CD_IS_ERROR = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "isError", + boolean.class); + static final MethodDescriptor CD_MISSING_VALUE = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "missingValue", + void.class, String.class, NoSuchElementException.class); + static final MethodDescriptor CD_RESET_ERROR = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "resetError", void.class); + static final MethodDescriptor CD_UNKNOWN = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "unknown", + void.class, NameIterator.class); + static final MethodDescriptor CD_UNKNOWN_RT = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "unknownRunTime", + void.class, NameIterator.class); + + static final MethodDescriptor CONVS_NEW_ARRAY_CONVERTER = MethodDescriptor.ofMethod(Converters.class, + "newArrayConverter", Converter.class, Converter.class, Class.class); + static final MethodDescriptor CONVS_NEW_COLLECTION_CONVERTER = MethodDescriptor.ofMethod(Converters.class, + "newCollectionConverter", Converter.class, Converter.class, IntFunction.class); + static final MethodDescriptor CONVS_NEW_OPTIONAL_CONVERTER = MethodDescriptor.ofMethod(Converters.class, + "newOptionalConverter", Converter.class, Converter.class); + static final MethodDescriptor CONVS_RANGE_VALUE_STRING_CONVERTER = MethodDescriptor.ofMethod(Converters.class, + "rangeValueStringConverter", Converter.class, Converter.class, String.class, boolean.class, String.class, + boolean.class); + static final MethodDescriptor CONVS_MINIMUM_VALUE_STRING_CONVERTER = MethodDescriptor.ofMethod(Converters.class, + "minimumValueStringConverter", Converter.class, Converter.class, String.class, boolean.class); + static final MethodDescriptor CONVS_MAXIMUM_VALUE_STRING_CONVERTER = MethodDescriptor.ofMethod(Converters.class, + "maximumValueStringConverter", Converter.class, Converter.class, String.class, boolean.class); + static final MethodDescriptor CONVS_PATTERN_CONVERTER = MethodDescriptor.ofMethod(Converters.class, + "patternConverter", Converter.class, Converter.class, Pattern.class); + + static final MethodDescriptor CPR_GET_CONFIG = MethodDescriptor.ofMethod(ConfigProviderResolver.class, "getConfig", + Config.class); + static final MethodDescriptor CPR_INSTANCE = MethodDescriptor.ofMethod(ConfigProviderResolver.class, "instance", + ConfigProviderResolver.class); + static final MethodDescriptor CPR_RELEASE_CONFIG = MethodDescriptor.ofMethod(ConfigProviderResolver.class, "releaseConfig", + void.class, Config.class); + + static final MethodDescriptor CU_LIST_FACTORY = MethodDescriptor.ofMethod(ConfigUtils.class, "listFactory", + IntFunction.class); + static final MethodDescriptor CU_SET_FACTORY = MethodDescriptor.ofMethod(ConfigUtils.class, "setFactory", + IntFunction.class); + static final MethodDescriptor CU_SORTED_SET_FACTORY = MethodDescriptor.ofMethod(ConfigUtils.class, "sortedSetFactory", + IntFunction.class); + static final MethodDescriptor CU_CONFIG_BUILDER = MethodDescriptor.ofMethod(ConfigUtils.class, "configBuilder", + SmallRyeConfigBuilder.class, boolean.class); + static final MethodDescriptor CU_ADD_SOURCE_PROVIDER = MethodDescriptor.ofMethod(ConfigUtils.class, "addSourceProvider", + void.class, SmallRyeConfigBuilder.class, ConfigSourceProvider.class); + + static final MethodDescriptor HM_NEW = MethodDescriptor.ofConstructor(HashMap.class); + static final MethodDescriptor HM_PUT = MethodDescriptor.ofMethod(HashMap.class, "put", Object.class, Object.class, + Object.class); + + static final MethodDescriptor ITRA_ITERATOR = MethodDescriptor.ofMethod(Iterable.class, "iterator", Iterator.class); + + static final MethodDescriptor ITR_HAS_NEXT = MethodDescriptor.ofMethod(Iterator.class, "hasNext", boolean.class); + static final MethodDescriptor ITR_NEXT = MethodDescriptor.ofMethod(Iterator.class, "next", Object.class); + + static final MethodDescriptor MAP_GET = MethodDescriptor.ofMethod(Map.class, "get", Object.class, Object.class); + static final MethodDescriptor MAP_PUT = MethodDescriptor.ofMethod(Map.class, "put", Object.class, Object.class, + Object.class); + + static final MethodDescriptor NI_GET_ALL_PREVIOUS_SEGMENTS = MethodDescriptor.ofMethod(NameIterator.class, + "getAllPreviousSegments", String.class); + static final MethodDescriptor NI_GET_NAME = MethodDescriptor.ofMethod(NameIterator.class, "getName", String.class); + static final MethodDescriptor NI_GET_PREVIOUS_SEGMENT = MethodDescriptor.ofMethod(NameIterator.class, "getPreviousSegment", + String.class); + static final MethodDescriptor NI_HAS_NEXT = MethodDescriptor.ofMethod(NameIterator.class, "hasNext", boolean.class); + static final MethodDescriptor NI_NEW_STRING = MethodDescriptor.ofConstructor(NameIterator.class, String.class); + static final MethodDescriptor NI_NEXT_EQUALS = MethodDescriptor.ofMethod(NameIterator.class, "nextSegmentEquals", + boolean.class, String.class); + static final MethodDescriptor NI_NEXT = MethodDescriptor.ofMethod(NameIterator.class, "next", void.class); + static final MethodDescriptor NI_PREVIOUS = MethodDescriptor.ofMethod(NameIterator.class, "previous", void.class); + static final MethodDescriptor NI_PREVIOUS_EQUALS = MethodDescriptor.ofMethod(NameIterator.class, "previousSegmentEquals", + boolean.class, String.class); + + static final MethodDescriptor OBJ_TO_STRING = MethodDescriptor.ofMethod(Object.class, "toString", String.class); + + static final MethodDescriptor OPT_EMPTY = MethodDescriptor.ofMethod(Optional.class, "empty", Optional.class); + static final MethodDescriptor OPT_GET = MethodDescriptor.ofMethod(Optional.class, "get", Object.class); + static final MethodDescriptor OPT_IS_PRESENT = MethodDescriptor.ofMethod(Optional.class, "isPresent", boolean.class); + static final MethodDescriptor OPT_OF = MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class); + + static final MethodDescriptor PCS_NEW = MethodDescriptor.ofConstructor(PropertiesConfigSource.class, + Map.class, String.class, int.class); + + static final MethodDescriptor PM_SET_RUNTIME_DEFAULT_PROFILE = MethodDescriptor.ofMethod(ProfileManager.class, + "setRuntimeDefaultProfile", void.class, String.class); + + static final MethodDescriptor SB_NEW = MethodDescriptor.ofConstructor(StringBuilder.class); + static final MethodDescriptor SB_NEW_STR = MethodDescriptor.ofConstructor(StringBuilder.class, String.class); + static final MethodDescriptor SB_APPEND_STRING = MethodDescriptor.ofMethod(StringBuilder.class, "append", + StringBuilder.class, String.class); + static final MethodDescriptor SB_APPEND_CHAR = MethodDescriptor.ofMethod(StringBuilder.class, "append", + StringBuilder.class, char.class); + static final MethodDescriptor SB_LENGTH = MethodDescriptor.ofMethod(StringBuilder.class, "length", + int.class); + static final MethodDescriptor SB_SET_LENGTH = MethodDescriptor.ofMethod(StringBuilder.class, "setLength", + void.class, int.class); + + static final MethodDescriptor QCF_SET_CONFIG = MethodDescriptor.ofMethod(QuarkusConfigFactory.class, "setConfig", + void.class, SmallRyeConfig.class); + + static final MethodDescriptor RTDVCS_NEW = MethodDescriptor.ofConstructor(RTDVCS_CLASS_NAME); + + static final MethodDescriptor SRC_GET_CONVERTER = MethodDescriptor.ofMethod(SmallRyeConfig.class, "getConverter", + Converter.class, Class.class); + static final MethodDescriptor SRC_GET_PROPERTY_NAMES = MethodDescriptor.ofMethod(SmallRyeConfig.class, "getPropertyNames", + Iterable.class); + static final MethodDescriptor SRC_GET_VALUE = MethodDescriptor.ofMethod(SmallRyeConfig.class, "getValue", + Object.class, String.class, Converter.class); + + static final MethodDescriptor SRCB_WITH_CONVERTER = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, + "withConverter", ConfigBuilder.class, Class.class, int.class, Converter.class); + static final MethodDescriptor SRCB_WITH_SOURCES = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, + "withSources", ConfigBuilder.class, ConfigSource[].class); + static final MethodDescriptor SRCB_BUILD = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, "build", + SmallRyeConfig.class); + + // todo: more space-efficient sorted map impl + static final MethodDescriptor TM_NEW = MethodDescriptor.ofConstructor(TreeMap.class); + + private RunTimeConfigurationGenerator() { + } + + public static void generate(BuildTimeConfigurationReader.ReadResult readResult, final ClassOutput classOutput, + final Map runTimeDefaults, List> additionalTypes) { + new GenerateOperation.Builder().setBuildTimeReadResult(readResult).setClassOutput(classOutput) + .setRunTimeDefaults(runTimeDefaults).setAdditionalTypes(additionalTypes).build().run(); + } + + static final class GenerateOperation implements AutoCloseable { + final AccessorFinder accessorFinder; + final ClassOutput classOutput; + final ClassCreator cc; + final MethodCreator clinit; + final BytecodeCreator converterSetup; + final MethodCreator readConfig; + final ResultHandle readConfigNameBuilder; + final ResultHandle clinitNameBuilder; + final BuildTimeConfigurationReader.ReadResult buildTimeConfigResult; + final ConfigPatternMap runTimePatternMap; + final List roots; + // default values given in the build configuration + final Map specifiedRunTimeDefaultValues; + final Map buildTimeRunTimeVisibleValues; + // default values produced by extensions via build item + final Map runTimeDefaults; + final Map enclosingMemberMethods = new HashMap<>(); + final Map, MethodDescriptor> groupInitMethods = new HashMap<>(); + final Map, FieldDescriptor> configRootsByType = new HashMap<>(); + final ResultHandle clinitConfig; + final Map> convertersToRegister = new HashMap<>(); + final List> additionalTypes; + /** + * Regular converters organized by type. Each converter is stored in a separate field. Some are used + * only at build time, some only at run time, and some at both times. + * Producing a native image will automatically delete the converters which are not used at run time from the + * final image. + */ + final Map convertersByType = new HashMap<>(); + /** + * Cache of things created in `clinit` which are then stored in fields, including config roots and converter + * instances. The result handles are usable only from `clinit`. + */ + final Map instanceCache = new HashMap<>(); + /** + * Converter fields have numeric names to keep space down. + */ + int converterIndex = 0; + + GenerateOperation(Builder builder) { + final BuildTimeConfigurationReader.ReadResult buildTimeReadResult = builder.buildTimeReadResult; + buildTimeConfigResult = Assert.checkNotNullParam("buildTimeReadResult", buildTimeReadResult); + specifiedRunTimeDefaultValues = Assert.checkNotNullParam("specifiedRunTimeDefaultValues", + buildTimeReadResult.getSpecifiedRunTimeDefaultValues()); + buildTimeRunTimeVisibleValues = Assert.checkNotNullParam("buildTimeRunTimeVisibleValues", + buildTimeReadResult.getBuildTimeRunTimeVisibleValues()); + classOutput = Assert.checkNotNullParam("classOutput", builder.getClassOutput()); + roots = Assert.checkNotNullParam("builder.roots", builder.getBuildTimeReadResult().getAllRoots()); + runTimeDefaults = Assert.checkNotNullParam("runTimeDefaults", builder.getRunTimeDefaults()); + additionalTypes = Assert.checkNotNullParam("additionalTypes", builder.getAdditionalTypes()); + cc = ClassCreator.builder().classOutput(classOutput).className(CONFIG_CLASS_NAME).setFinal(true).build(); + // not instantiable + try (MethodCreator mc = cc.getMethodCreator(MethodDescriptor.ofConstructor(CONFIG_CLASS_NAME))) { + mc.setModifiers(Opcodes.ACC_PRIVATE); + mc.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), mc.getThis()); + mc.returnValue(null); + } + + // create + clinit = cc.getMethodCreator(MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "", void.class)); + clinit.setModifiers(Opcodes.ACC_STATIC); + clinit.invokeStaticMethod(PM_SET_RUNTIME_DEFAULT_PROFILE, clinit.load(ProfileManager.getActiveProfile())); + clinitNameBuilder = clinit.newInstance(SB_NEW); + clinit.invokeVirtualMethod(SB_APPEND_STRING, clinitNameBuilder, clinit.load("quarkus")); + + // create the map for build time config source + final ResultHandle buildTimeValues = clinit.newInstance(HM_NEW); + for (Map.Entry entry : buildTimeRunTimeVisibleValues.entrySet()) { + clinit.invokeVirtualMethod(HM_PUT, buildTimeValues, clinit.load(entry.getKey()), clinit.load(entry.getValue())); + } + + // the build time config source field, to feed into the run time config + cc.getFieldCreator(C_BUILD_TIME_CONFIG_SOURCE) + .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + final ResultHandle buildTimeConfigSource = clinit.newInstance(PCS_NEW, buildTimeValues, + clinit.load("Build time config"), clinit.load(100)); + clinit.writeStaticField(C_BUILD_TIME_CONFIG_SOURCE, buildTimeConfigSource); + + // the build time run time visible default values config source + cc.getFieldCreator(C_BUILD_TIME_RUN_TIME_DEFAULTS_CONFIG_SOURCE) + .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + final ResultHandle buildTimeRunTimeDefaultValuesConfigSource = clinit.newInstance(BTRTDVCS_NEW); + clinit.writeStaticField(C_BUILD_TIME_RUN_TIME_DEFAULTS_CONFIG_SOURCE, buildTimeRunTimeDefaultValuesConfigSource); + + // the run time default values config source + cc.getFieldCreator(C_RUN_TIME_DEFAULTS_CONFIG_SOURCE) + .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + clinit.writeStaticField(C_RUN_TIME_DEFAULTS_CONFIG_SOURCE, clinit.newInstance(RTDVCS_NEW)); + + // the build time config, which is for user use only (not used by us other than for loading converters) + final ResultHandle buildTimeBuilder = clinit.invokeStaticMethod(CU_CONFIG_BUILDER, clinit.load(true)); + final ResultHandle array = clinit.newArray(ConfigSource[].class, clinit.load(2)); + // build time values + clinit.writeArrayValue(array, 0, buildTimeConfigSource); + // build time defaults + clinit.writeArrayValue(array, 1, buildTimeRunTimeDefaultValuesConfigSource); + clinit.invokeVirtualMethod(SRCB_WITH_SOURCES, buildTimeBuilder, array); + clinitConfig = clinit.checkCast(clinit.invokeVirtualMethod(SRCB_BUILD, buildTimeBuilder), + SmallRyeConfig.class); + + // block for converter setup + converterSetup = clinit.createScope(); + // create readConfig + readConfig = cc.getMethodCreator(C_READ_CONFIG); + // the readConfig name builder + readConfigNameBuilder = readConfig.newInstance(SB_NEW); + readConfig.invokeVirtualMethod(SB_APPEND_STRING, readConfigNameBuilder, readConfig.load("quarkus")); + runTimePatternMap = buildTimeReadResult.getRunTimePatternMap(); + accessorFinder = new AccessorFinder(); + } + + public void run() { + // in clinit, load the build-time config + + // make the build time config global until we read the run time config - + // at run time (when we're ready) we update the factory and then release the build time config + clinit.invokeStaticMethod(QCF_SET_CONFIG, clinitConfig); + // release any previous configuration + final ResultHandle clinitCpr = clinit.invokeStaticMethod(CPR_INSTANCE); + try (TryBlock getConfigTry = clinit.tryBlock()) { + final ResultHandle initialConfigHandle = getConfigTry.invokeVirtualMethod(CPR_GET_CONFIG, + clinitCpr); + getConfigTry.invokeVirtualMethod(CPR_RELEASE_CONFIG, clinitCpr, initialConfigHandle); + // ignore + getConfigTry.addCatch(IllegalStateException.class); + } + + // fill roots map + for (RootDefinition root : roots) { + configRootsByType.put(root.getConfigurationClass(), root.getDescriptor()); + } + + // generate the parse methods and populate converters + + final ConfigPatternMap buildTimePatternMap = buildTimeConfigResult.getBuildTimePatternMap(); + final ConfigPatternMap buildTimeRunTimePatternMap = buildTimeConfigResult + .getBuildTimeRunTimePatternMap(); + final ConfigPatternMap runTimePatternMap = buildTimeConfigResult.getRunTimePatternMap(); + + final BiFunction combinator = (a, b) -> a == null ? b : a; + final ConfigPatternMap buildTimeRunTimeIgnored = ConfigPatternMap.merge(buildTimePatternMap, + runTimePatternMap, combinator); + final ConfigPatternMap runTimeIgnored = ConfigPatternMap.merge(buildTimePatternMap, + buildTimeRunTimePatternMap, combinator); + + final MethodDescriptor siParserBody = generateParserBody(buildTimeRunTimePatternMap, buildTimeRunTimeIgnored, + new StringBuilder("siParseKey"), false, false); + final MethodDescriptor rtParserBody = generateParserBody(runTimePatternMap, runTimeIgnored, + new StringBuilder("rtParseKey"), false, true); + + // create the run time config + final ResultHandle runTimeBuilder = readConfig.invokeStaticMethod(CU_CONFIG_BUILDER, readConfig.load(true)); + + // add in our run time only config source provider + readConfig.invokeStaticMethod(CU_ADD_SOURCE_PROVIDER, runTimeBuilder, readConfig.newInstance( + MethodDescriptor.ofConstructor("io.quarkus.runtime.generated.ConfigSourceProviderImpl"))); + + // create the map for run time specified values config source + final ResultHandle specifiedRunTimeValues = clinit.newInstance(HM_NEW); + for (Map.Entry entry : specifiedRunTimeDefaultValues.entrySet()) { + clinit.invokeVirtualMethod(HM_PUT, specifiedRunTimeValues, clinit.load(entry.getKey()), + clinit.load(entry.getValue())); + } + for (Map.Entry entry : runTimeDefaults.entrySet()) { + if (!specifiedRunTimeDefaultValues.containsKey(entry.getKey())) { + // only add entry if the user didn't override it + clinit.invokeVirtualMethod(HM_PUT, specifiedRunTimeValues, clinit.load(entry.getKey()), + clinit.load(entry.getValue())); + } + } + final ResultHandle specifiedRunTimeSource = clinit.newInstance(PCS_NEW, specifiedRunTimeValues, + clinit.load("Specified default values"), clinit.load(Integer.MIN_VALUE + 100)); + cc.getFieldCreator(C_SPECIFIED_RUN_TIME_CONFIG_SOURCE).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + clinit.writeStaticField(C_SPECIFIED_RUN_TIME_CONFIG_SOURCE, specifiedRunTimeSource); + + // add in our custom sources + final ResultHandle array = readConfig.newArray(ConfigSource[].class, readConfig.load(4)); + // build time config (expanded values) + readConfig.writeArrayValue(array, 0, readConfig.readStaticField(C_BUILD_TIME_CONFIG_SOURCE)); + // specified run time config default values + readConfig.writeArrayValue(array, 1, readConfig.readStaticField(C_SPECIFIED_RUN_TIME_CONFIG_SOURCE)); + // run time config default values + readConfig.writeArrayValue(array, 2, readConfig.readStaticField(C_RUN_TIME_DEFAULTS_CONFIG_SOURCE)); + // build time run time visible default config source + readConfig.writeArrayValue(array, 3, readConfig.readStaticField(C_BUILD_TIME_RUN_TIME_DEFAULTS_CONFIG_SOURCE)); + + // add in known converters + for (Class additionalType : additionalTypes) { + ConverterType type = new Leaf(additionalType, null); + FieldDescriptor fd = convertersByType.get(type); + if (fd == null) { + // it's an unknown + final ResultHandle clazzHandle = converterSetup.loadClass(additionalType); + fd = FieldDescriptor.of(cc.getClassName(), "conv$" + converterIndex++, Converter.class); + ResultHandle converter = converterSetup.invokeVirtualMethod(SRC_GET_CONVERTER, clinitConfig, clazzHandle); + cc.getFieldCreator(fd).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + converterSetup.writeStaticField(fd, converter); + convertersByType.put(type, fd); + instanceCache.put(fd, converter); + convertersToRegister.put(fd, additionalType); + } + } + if (!convertersToRegister.isEmpty()) { + for (Map.Entry> entry : convertersToRegister.entrySet()) { + final FieldDescriptor descriptor = entry.getKey(); + final Class type = entry.getValue(); + readConfig.invokeVirtualMethod(SRCB_WITH_CONVERTER, runTimeBuilder, readConfig.loadClass(type), + readConfig.load(100), readConfig.readStaticField(descriptor)); + } + } + + // put them in the builder + readConfig.invokeVirtualMethod(SRCB_WITH_SOURCES, runTimeBuilder, array); + + final ResultHandle runTimeConfig = readConfig.invokeVirtualMethod(SRCB_BUILD, runTimeBuilder); + // install run time config + readConfig.invokeStaticMethod(QCF_SET_CONFIG, runTimeConfig); + // now invalidate the cached config, so the next one to load the config gets the new one + final ResultHandle configProviderResolver = readConfig.invokeStaticMethod(CPR_INSTANCE); + try (TryBlock getConfigTry = readConfig.tryBlock()) { + final ResultHandle initialConfigHandle = getConfigTry.invokeVirtualMethod(CPR_GET_CONFIG, + configProviderResolver); + getConfigTry.invokeVirtualMethod(CPR_RELEASE_CONFIG, configProviderResolver, initialConfigHandle); + // ignore + getConfigTry.addCatch(IllegalStateException.class); + } + + final ResultHandle clInitOldLen = clinit.invokeVirtualMethod(SB_LENGTH, clinitNameBuilder); + final ResultHandle rcOldLen = readConfig.invokeVirtualMethod(SB_LENGTH, readConfigNameBuilder); + + // generate eager config read (both build and run time at once) + for (RootDefinition root : roots) { + // common things for all config phases + final Class configurationClass = root.getConfigurationClass(); + FieldDescriptor rootFieldDescriptor = root.getDescriptor(); + + // Get or generate group init method + MethodDescriptor initGroup = generateInitGroup(root); + + final MethodDescriptor ctor = accessorFinder + .getConstructorFor(MethodDescriptor.ofConstructor(configurationClass)); + + // specific actions based on config phase + String rootName = root.getRootName(); + if (root.getConfigPhase() == ConfigPhase.BUILD_AND_RUN_TIME_FIXED) { + // config root field is final; we initialize it from clinit + cc.getFieldCreator(rootFieldDescriptor) + .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + // construct instance in + final ResultHandle instance = clinit.invokeStaticMethod(ctor); + // assign instance to field + clinit.writeStaticField(rootFieldDescriptor, instance); + instanceCache.put(rootFieldDescriptor, instance); + // eager init as appropriate + if (!rootName.isEmpty()) { + clinit.invokeVirtualMethod(SB_APPEND_CHAR, clinitNameBuilder, clinit.load('.')); + clinit.invokeVirtualMethod(SB_APPEND_STRING, clinitNameBuilder, clinit.load(rootName)); + } + clinit.invokeStaticMethod(initGroup, clinitConfig, clinitNameBuilder, instance); + clinit.invokeVirtualMethod(SB_SET_LENGTH, clinitNameBuilder, clInitOldLen); + } else if (root.getConfigPhase() == ConfigPhase.RUN_TIME) { + // config root field is volatile; we initialize and read config from the readConfig method + cc.getFieldCreator(rootFieldDescriptor) + .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_VOLATILE); + // construct instance in readConfig + final ResultHandle instance = readConfig.invokeStaticMethod(ctor); + // assign instance to field + readConfig.writeStaticField(rootFieldDescriptor, instance); + if (!rootName.isEmpty()) { + readConfig.invokeVirtualMethod(SB_APPEND_CHAR, readConfigNameBuilder, readConfig.load('.')); + readConfig.invokeVirtualMethod(SB_APPEND_STRING, readConfigNameBuilder, + readConfig.load(rootName)); + } + readConfig.invokeStaticMethod(initGroup, runTimeConfig, readConfigNameBuilder, instance); + readConfig.invokeVirtualMethod(SB_SET_LENGTH, readConfigNameBuilder, rcOldLen); + } else { + assert root.getConfigPhase() == ConfigPhase.BUILD_TIME; + // ignore explicitly for now (no eager read for these) + } + } + + ResultHandle nameSet; + ResultHandle iterator; + + // generate sweep for clinit + nameSet = clinit.invokeVirtualMethod(SRC_GET_PROPERTY_NAMES, clinitConfig); + iterator = clinit.invokeInterfaceMethod(ITRA_ITERATOR, nameSet); + + try (BytecodeCreator sweepLoop = clinit.createScope()) { + try (BytecodeCreator hasNext = sweepLoop.ifNonZero(sweepLoop.invokeInterfaceMethod(ITR_HAS_NEXT, iterator)) + .trueBranch()) { + + final ResultHandle key = hasNext.checkCast(hasNext.invokeInterfaceMethod(ITR_NEXT, iterator), String.class); + // NameIterator keyIter = new NameIterator(key); + final ResultHandle keyIter = hasNext.newInstance(NI_NEW_STRING, key); + // if (! keyIter.hasNext()) continue sweepLoop; + hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_HAS_NEXT, keyIter)).falseBranch().continueScope(sweepLoop); + // if (! keyIter.nextSegmentEquals("quarkus")) continue sweepLoop; + hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, hasNext.load("quarkus"))) + .falseBranch().continueScope(sweepLoop); + // keyIter.next(); // skip "quarkus" + hasNext.invokeVirtualMethod(NI_NEXT, keyIter); + // parse(config, keyIter); + hasNext.invokeStaticMethod(siParserBody, clinitConfig, keyIter); + // continue sweepLoop; + hasNext.continueScope(sweepLoop); + } + } + + // generate sweep for run time + nameSet = readConfig.invokeVirtualMethod(SRC_GET_PROPERTY_NAMES, runTimeConfig); + iterator = readConfig.invokeInterfaceMethod(ITRA_ITERATOR, nameSet); + + try (BytecodeCreator sweepLoop = readConfig.createScope()) { + try (BytecodeCreator hasNext = sweepLoop.ifNonZero(sweepLoop.invokeInterfaceMethod(ITR_HAS_NEXT, iterator)) + .trueBranch()) { + + final ResultHandle key = hasNext.checkCast(hasNext.invokeInterfaceMethod(ITR_NEXT, iterator), String.class); + // NameIterator keyIter = new NameIterator(key); + final ResultHandle keyIter = hasNext.newInstance(NI_NEW_STRING, key); + // if (! keyIter.hasNext()) continue sweepLoop; + hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_HAS_NEXT, keyIter)).falseBranch().continueScope(sweepLoop); + // if (! keyIter.nextSegmentEquals("quarkus")) continue sweepLoop; + hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, hasNext.load("quarkus"))) + .falseBranch().continueScope(sweepLoop); + // keyIter.next(); // skip "quarkus" + hasNext.invokeVirtualMethod(NI_NEXT, keyIter); + // parse(config, keyIter); + hasNext.invokeStaticMethod(rtParserBody, runTimeConfig, keyIter); + // continue sweepLoop; + hasNext.continueScope(sweepLoop); + } + } + + // generate ensure-initialized method + try (MethodCreator mc = cc.getMethodCreator(C_ENSURE_INITIALIZED)) { + mc.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); + mc.returnValue(null); + } + + // generate run time entry point + try (MethodCreator mc = cc.getMethodCreator(C_CREATE_RUN_TIME_CONFIG)) { + mc.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); + ResultHandle instance = mc.newInstance(MethodDescriptor.ofConstructor(CONFIG_CLASS_NAME)); + mc.invokeVirtualMethod(C_READ_CONFIG, instance); + mc.returnValue(null); + } + + // wrap it up + final BytecodeCreator isError = readConfig.ifNonZero(readConfig.invokeStaticMethod(CD_IS_ERROR)).trueBranch(); + ResultHandle niceErrorMessage = isError + .invokeStaticMethod( + MethodDescriptor.ofMethod(ConfigDiagnostic.class, "getNiceErrorMessage", String.class)); + readConfig.invokeStaticMethod(CD_RESET_ERROR); + + // throw the proper exception + final ResultHandle finalErrorMessageBuilder = isError.newInstance(SB_NEW); + isError.invokeVirtualMethod(SB_APPEND_STRING, finalErrorMessageBuilder, isError + .load("One or more configuration errors has prevented the application from starting. The errors are:\n")); + isError.invokeVirtualMethod(SB_APPEND_STRING, finalErrorMessageBuilder, niceErrorMessage); + final ResultHandle finalErrorMessage = isError.invokeVirtualMethod(OBJ_TO_STRING, finalErrorMessageBuilder); + final ResultHandle configurationException = isError + .newInstance(MethodDescriptor.ofConstructor(ConfigurationException.class, String.class), finalErrorMessage); + final ResultHandle emptyStackTraceElement = isError.newArray(StackTraceElement.class, isError.load(0)); + // empty out the stack trace in order to not make the configuration errors more visible (the stack trace contains generated classes anyway that don't provide any value) + isError.invokeVirtualMethod( + MethodDescriptor.ofMethod(ConfigurationException.class, "setStackTrace", void.class, + StackTraceElement[].class), + configurationException, emptyStackTraceElement); + isError.throwException(configurationException); + + readConfig.returnValue(null); + readConfig.close(); + clinit.returnValue(null); + clinit.close(); + cc.close(); + + // generate run time default values config source class + try (ClassCreator dvcc = ClassCreator.builder().classOutput(classOutput).className(RTDVCS_CLASS_NAME) + .superClass(AbstractRawDefaultConfigSource.class).setFinal(true).build()) { + // implements abstract method AbstractRawDefaultConfigSource#getValue(NameIterator) + try (MethodCreator mc = dvcc.getMethodCreator("getValue", String.class, NameIterator.class)) { + final ResultHandle keyIter = mc.getMethodParam(0); + final MethodDescriptor md = generateDefaultValueParse(dvcc, runTimePatternMap, + new StringBuilder("getDefaultFor")); + if (md != null) { + // there is at least one default value + final BranchResult if1 = mc.ifNonZero(mc.invokeVirtualMethod(NI_HAS_NEXT, keyIter)); + try (BytecodeCreator true1 = if1.trueBranch()) { + true1.invokeVirtualMethod(NI_NEXT, keyIter); + final BranchResult if2 = true1 + .ifNonZero(true1.invokeVirtualMethod(NI_PREVIOUS_EQUALS, keyIter, true1.load("quarkus"))); + try (BytecodeCreator true2 = if2.trueBranch()) { + final ResultHandle result = true2.invokeVirtualMethod( + md, mc.getThis(), keyIter); + true2.returnValue(result); + } + } + } + + mc.returnValue(mc.loadNull()); + } + } + + // generate build time run time visible default values config source class + try (ClassCreator dvcc = ClassCreator.builder().classOutput(classOutput).className(BTRTDVCS_CLASS_NAME) + .superClass(AbstractRawDefaultConfigSource.class).setFinal(true).build()) { + // implements abstract method AbstractRawDefaultConfigSource#getValue(NameIterator) + try (MethodCreator mc = dvcc.getMethodCreator("getValue", String.class, NameIterator.class)) { + final ResultHandle keyIter = mc.getMethodParam(0); + final MethodDescriptor md = generateDefaultValueParse(dvcc, buildTimeRunTimePatternMap, + new StringBuilder("getDefaultFor")); + if (md != null) { + // there is at least one default value + final BranchResult if1 = mc.ifNonZero(mc.invokeVirtualMethod(NI_HAS_NEXT, keyIter)); + try (BytecodeCreator true1 = if1.trueBranch()) { + true1.invokeVirtualMethod(NI_NEXT, keyIter); + final BranchResult if2 = true1 + .ifNonZero(true1.invokeVirtualMethod(NI_PREVIOUS_EQUALS, keyIter, true1.load("quarkus"))); + try (BytecodeCreator true2 = if2.trueBranch()) { + final ResultHandle result = true2.invokeVirtualMethod( + md, mc.getThis(), keyIter); + true2.returnValue(result); + } + } + } + + mc.returnValue(mc.loadNull()); + } + } + } + + private MethodDescriptor generateInitGroup(ClassDefinition definition) { + final Class clazz = definition.getConfigurationClass(); + MethodDescriptor methodDescriptor = groupInitMethods.get(clazz); + if (methodDescriptor != null) { + return methodDescriptor; + } + methodDescriptor = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "initGroup$" + clazz.getName().replace('.', '$'), + void.class, SmallRyeConfig.class, StringBuilder.class, Object.class); + final MethodCreator bc = cc.getMethodCreator(methodDescriptor).setModifiers(Opcodes.ACC_STATIC); + final ResultHandle config = bc.getMethodParam(0); + // on entry, nameBuilder is our name + final ResultHandle nameBuilder = bc.getMethodParam(1); + final ResultHandle instance = bc.getMethodParam(2); + final ResultHandle length = bc.invokeVirtualMethod(SB_LENGTH, nameBuilder); + for (ClassDefinition.ClassMember member : definition.getMembers()) { + // common setup + final String propertyName = member.getPropertyName(); + final MethodDescriptor setter = accessorFinder.getSetterFor(member.getDescriptor()); + if (!propertyName.isEmpty()) { + // append the property name + bc.invokeVirtualMethod(SB_APPEND_CHAR, nameBuilder, bc.load('.')); + bc.invokeVirtualMethod(SB_APPEND_STRING, nameBuilder, bc.load(propertyName)); + } + if (member instanceof ClassDefinition.ItemMember) { + ClassDefinition.ItemMember leafMember = (ClassDefinition.ItemMember) member; + final FieldDescriptor convField = getOrCreateConverterInstance(leafMember.getField()); + final ResultHandle name = bc.invokeVirtualMethod(OBJ_TO_STRING, nameBuilder); + final ResultHandle converter = bc.readStaticField(convField); + try (TryBlock tryBlock = bc.tryBlock()) { + final ResultHandle val = tryBlock.invokeVirtualMethod(SRC_GET_VALUE, config, name, converter); + tryBlock.invokeStaticMethod(setter, instance, val); + try (CatchBlockCreator catchBadValue = tryBlock.addCatch(IllegalArgumentException.class)) { + catchBadValue.invokeStaticMethod(CD_INVALID_VALUE, name, catchBadValue.getCaughtException()); + } + try (CatchBlockCreator catchNoValue = tryBlock.addCatch(NoSuchElementException.class)) { + catchNoValue.invokeStaticMethod(CD_MISSING_VALUE, name, catchNoValue.getCaughtException()); + } + } + } else if (member instanceof ClassDefinition.GroupMember) { + ClassDefinition.GroupMember groupMember = (ClassDefinition.GroupMember) member; + if (groupMember.isOptional()) { + bc.invokeStaticMethod(setter, instance, bc.invokeStaticMethod(OPT_EMPTY)); + } else { + final GroupDefinition groupDefinition = groupMember.getGroupDefinition(); + final MethodDescriptor nested = generateInitGroup(groupDefinition); + final MethodDescriptor ctor = accessorFinder + .getConstructorFor(MethodDescriptor.ofConstructor(groupDefinition.getConfigurationClass())); + final ResultHandle nestedInstance = bc.invokeStaticMethod(ctor); + bc.invokeStaticMethod(nested, config, nameBuilder, nestedInstance); + bc.invokeStaticMethod(setter, instance, nestedInstance); + } + } else { + assert member instanceof ClassDefinition.MapMember; + final ResultHandle map = bc.newInstance(TM_NEW); + bc.invokeStaticMethod(setter, instance, map); + } + if (!propertyName.isEmpty()) { + // restore length + bc.invokeVirtualMethod(SB_SET_LENGTH, nameBuilder, length); + } + } + bc.returnValue(null); + groupInitMethods.put(clazz, methodDescriptor); + return methodDescriptor; + } + + private static MethodDescriptor generateDefaultValueParse(final ClassCreator dvcc, + final ConfigPatternMap keyMap, final StringBuilder methodName) { + + final Container matched = keyMap.getMatched(); + final boolean hasDefault; + if (matched != null) { + final ClassDefinition.ClassMember member = matched.getClassMember(); + // matched members *must* be item members + assert member instanceof ClassDefinition.ItemMember; + ClassDefinition.ItemMember itemMember = (ClassDefinition.ItemMember) member; + hasDefault = itemMember.getDefaultValue() != null; + } else { + hasDefault = false; + } + + final Iterable names = keyMap.childNames(); + final Map children = new HashMap<>(); + MethodDescriptor wildCard = null; + for (String name : names) { + final int length = methodName.length(); + if (name.equals(ConfigPatternMap.WILD_CARD)) { + methodName.append(":*"); + wildCard = generateDefaultValueParse(dvcc, keyMap.getChild(ConfigPatternMap.WILD_CARD), methodName); + } else { + methodName.append(':').append(name); + final MethodDescriptor value = generateDefaultValueParse(dvcc, keyMap.getChild(name), methodName); + if (value != null) { + children.put(name, value); + } + } + methodName.setLength(length); + } + if (children.isEmpty() && wildCard == null && !hasDefault) { + // skip parse trees with no default values in them + return null; + } + + try (MethodCreator body = dvcc.getMethodCreator(methodName.toString(), String.class, NameIterator.class)) { + body.setModifiers(Opcodes.ACC_PRIVATE); + + final ResultHandle keyIter = body.getMethodParam(0); + // if we've matched the whole thing... + // if (! keyIter.hasNext()) { + try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)) + .falseBranch()) { + if (matched != null) { + final ClassDefinition.ClassMember member = matched.getClassMember(); + // matched members *must* be item members + assert member instanceof ClassDefinition.ItemMember; + ClassDefinition.ItemMember itemMember = (ClassDefinition.ItemMember) member; + // match? + final String defaultValue = itemMember.getDefaultValue(); + if (defaultValue != null) { + // matched with default value + // return "defaultValue"; + matchedBody.returnValue(matchedBody.load(defaultValue)); + } else { + // matched but no default value + // return null; + matchedBody.returnValue(matchedBody.loadNull()); + } + } else { + // no match + // return null; + matchedBody.returnValue(matchedBody.loadNull()); + } + } + // } + // branches for each next-string + for (String name : children.keySet()) { + // TODO: string switch + // if (keyIter.nextSegmentEquals(name)) { + try (BytecodeCreator nameMatched = body + .ifNonZero(body.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, body.load(name))).trueBranch()) { + // keyIter.next(); + nameMatched.invokeVirtualMethod(NI_NEXT, keyIter); + // (generated recursive) + // result = getDefault$..$name(keyIter); + ResultHandle result = nameMatched.invokeVirtualMethod(children.get(name), body.getThis(), keyIter); + // return result; + nameMatched.returnValue(result); + } + // } + } + if (wildCard != null) { + // consume and parse + try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)) + .trueBranch()) { + // keyIter.next(); + matchedBody.invokeVirtualMethod(NI_NEXT, keyIter); + // (generated recursive) + // result = getDefault$..$*(keyIter); + final ResultHandle result = matchedBody.invokeVirtualMethod(wildCard, body.getThis(), keyIter); + // return result; + matchedBody.returnValue(result); + } + } + // unknown + // return null; + body.returnValue(body.loadNull()); + + return body.getMethodDescriptor(); + } + } + + private MethodDescriptor generateParserBody(final ConfigPatternMap keyMap, + final ConfigPatternMap ignoredMap, final StringBuilder methodName, final boolean dynamic, + final boolean isRunTime) { + try (MethodCreator body = cc.getMethodCreator(methodName.toString(), void.class, + SmallRyeConfig.class, NameIterator.class)) { + body.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); + final ResultHandle config = body.getMethodParam(0); + final ResultHandle keyIter = body.getMethodParam(1); + final Container matched = keyMap == null ? null : keyMap.getMatched(); + final Object ignoreMatched = ignoredMap == null ? null : ignoredMap.getMatched(); + // if (! keyIter.hasNext()) { + try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)) + .falseBranch()) { + if (matched != null) { + final ClassDefinition.ClassMember member = matched.getClassMember(); + // matched members *must* be item members + assert member instanceof ClassDefinition.ItemMember; + ClassDefinition.ItemMember itemMember = (ClassDefinition.ItemMember) member; + + if (matched instanceof FieldContainer) { + final FieldContainer fieldContainer = (FieldContainer) matched; + if (dynamic) { + if (!itemMember.getPropertyName().isEmpty()) { + // consume segment + matchedBody.invokeVirtualMethod(NI_PREVIOUS, keyIter); + } + // we have to get or create all containing (and contained) groups of this member + matchedBody.invokeStaticMethod(generateGetEnclosing(fieldContainer, isRunTime), keyIter, + config); + } + // else ignore (already populated eagerly) + } else { + assert matched instanceof MapContainer; + MapContainer mapContainer = (MapContainer) matched; + // map leafs are always dynamic + final ResultHandle lastSeg = matchedBody.invokeVirtualMethod(NI_GET_PREVIOUS_SEGMENT, keyIter); + matchedBody.invokeVirtualMethod(NI_PREVIOUS, keyIter); + final ResultHandle mapHandle = matchedBody + .invokeStaticMethod(generateGetEnclosing(mapContainer, isRunTime), keyIter, config); + // populate the map + final Field field = mapContainer.findField(); + final FieldDescriptor fd = getOrCreateConverterInstance(field); + final ResultHandle key = matchedBody.invokeVirtualMethod(NI_GET_NAME, keyIter); + final ResultHandle converter = matchedBody.readStaticField(fd); + final ResultHandle value = matchedBody.invokeVirtualMethod(SRC_GET_VALUE, config, key, converter); + matchedBody.invokeInterfaceMethod(MAP_PUT, mapHandle, lastSeg, value); + } + } else if (ignoreMatched == null) { + // name is unknown + matchedBody.invokeStaticMethod(isRunTime ? CD_UNKNOWN_RT : CD_UNKNOWN, keyIter); + } + // return; + matchedBody.returnValue(null); + } + // } + boolean hasWildCard = false; + // branches for each next-string + if (keyMap != null) { + final Iterable names = keyMap.childNames(); + for (String name : names) { + if (name.equals(ConfigPatternMap.WILD_CARD)) { + hasWildCard = true; + } else { + // TODO: string switch + // if (keyIter.nextSegmentEquals(name)) { + try (BytecodeCreator nameMatched = body + .ifNonZero(body.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, body.load(name))) + .trueBranch()) { + // keyIter.next(); + nameMatched.invokeVirtualMethod(NI_NEXT, keyIter); + // (generated recursive) + final int length = methodName.length(); + methodName.append(':').append(name); + nameMatched.invokeStaticMethod( + generateParserBody(keyMap.getChild(name), + ignoredMap == null ? null : ignoredMap.getChild(name), methodName, dynamic, + isRunTime), + config, keyIter); + methodName.setLength(length); + // return; + nameMatched.returnValue(null); + } + // } + } + } + } + // branches for each ignored child + if (ignoredMap != null) { + final Iterable names = ignoredMap.childNames(); + for (String name : names) { + if (name.equals(ConfigPatternMap.WILD_CARD)) { + hasWildCard = true; + } else { + final ConfigPatternMap keyChildMap = keyMap == null ? null : keyMap.getChild(name); + if (keyChildMap != null) { + // we already did this one + continue; + } + // TODO: string switch + // if (keyIter.nextSegmentEquals(name)) { + try (BytecodeCreator nameMatched = body + .ifNonZero(body.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, body.load(name))) + .trueBranch()) { + // keyIter.next(); + nameMatched.invokeVirtualMethod(NI_NEXT, keyIter); + // (generated recursive) + final int length = methodName.length(); + methodName.append(':').append(name); + nameMatched.invokeStaticMethod( + generateParserBody(null, ignoredMap.getChild(name), methodName, false, isRunTime), + config, keyIter); + methodName.setLength(length); + // return; + nameMatched.returnValue(null); + } + // } + } + } + } + if (hasWildCard) { + assert keyMap != null || ignoredMap != null; + // consume and parse + try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)) + .trueBranch()) { + // keyIter.next(); + matchedBody.invokeVirtualMethod(NI_NEXT, keyIter); + // (generated recursive) + final int length = methodName.length(); + methodName.append(":*"); + matchedBody.invokeStaticMethod( + generateParserBody(keyMap == null ? null : keyMap.getChild(ConfigPatternMap.WILD_CARD), + ignoredMap == null ? null : ignoredMap.getChild(ConfigPatternMap.WILD_CARD), + methodName, + true, isRunTime), + config, keyIter); + methodName.setLength(length); + // return; + matchedBody.returnValue(null); + } + } + body.invokeStaticMethod(isRunTime ? CD_UNKNOWN_RT : CD_UNKNOWN, keyIter); + body.returnValue(null); + return body.getMethodDescriptor(); + } + } + + private MethodDescriptor generateGetEnclosing(final FieldContainer matchNode, final boolean isRunTime) { + // name iterator cursor is placed BEFORE the field name on entry + MethodDescriptor md = enclosingMemberMethods.get(matchNode); + if (md != null) { + return md; + } + md = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, + (isRunTime ? "rt" : "si") + "GetEnclosing:" + matchNode.getCombinedName(), Object.class, + NameIterator.class, SmallRyeConfig.class); + try (MethodCreator mc = cc.getMethodCreator(md)) { + mc.setModifiers(Opcodes.ACC_STATIC); + final ResultHandle keyIter = mc.getMethodParam(0); + final ResultHandle config = mc.getMethodParam(1); + final ClassDefinition.ClassMember member = matchNode.getClassMember(); + final Container parent = matchNode.getParent(); + if (parent == null) { + // it's a root + final RootDefinition definition = (RootDefinition) member.getEnclosingDefinition(); + FieldDescriptor fieldDescriptor = configRootsByType.get(definition.getConfigurationClass()); + assert fieldDescriptor != null : "Field descriptor defined for " + definition.getConfigurationClass(); + mc.returnValue(mc.readStaticField(fieldDescriptor)); + } else if (parent instanceof FieldContainer) { + // get the parent + final FieldContainer fieldContainer = (FieldContainer) parent; + final ClassDefinition.ClassMember classMember = fieldContainer.getClassMember(); + if (!classMember.getPropertyName().isEmpty()) { + // consume segment + mc.invokeVirtualMethod(NI_PREVIOUS, keyIter); + } + final ResultHandle enclosing = mc.invokeStaticMethod(generateGetEnclosing(fieldContainer, isRunTime), + keyIter, config); + final MethodDescriptor getter = accessorFinder.getGetterFor(classMember.getDescriptor()); + final ResultHandle fieldVal = mc.invokeStaticMethod(getter, enclosing); + final AssignableResultHandle group = mc.createVariable(Object.class); + if (classMember instanceof ClassDefinition.GroupMember + && ((ClassDefinition.GroupMember) classMember).isOptional()) { + final BranchResult isPresent = mc.ifNonZero(mc.invokeVirtualMethod(OPT_IS_PRESENT, fieldVal)); + final BytecodeCreator trueBranch = isPresent.trueBranch(); + final BytecodeCreator falseBranch = isPresent.falseBranch(); + // it already exists + trueBranch.assign(group, trueBranch.invokeVirtualMethod(OPT_GET, fieldVal)); + // it doesn't exist, recreate it + final MethodDescriptor ctor = accessorFinder.getConstructorFor( + MethodDescriptor.ofConstructor(member.getEnclosingDefinition().getConfigurationClass())); + final ResultHandle instance = falseBranch.invokeStaticMethod(ctor); + final ResultHandle precedingKey = falseBranch.invokeVirtualMethod(NI_GET_ALL_PREVIOUS_SEGMENTS, + keyIter); + final ResultHandle nameBuilder = falseBranch.newInstance(SB_NEW_STR, precedingKey); + falseBranch.invokeStaticMethod(generateInitGroup(member.getEnclosingDefinition()), config, nameBuilder, + instance); + final MethodDescriptor setter = accessorFinder.getSetterFor(classMember.getDescriptor()); + falseBranch.invokeStaticMethod(setter, fieldVal, falseBranch.invokeStaticMethod(OPT_OF, instance)); + falseBranch.assign(group, instance); + } else { + mc.assign(group, fieldVal); + } + if (!classMember.getPropertyName().isEmpty()) { + // restore + mc.invokeVirtualMethod(NI_NEXT, keyIter); + } + mc.returnValue(group); + } else { + assert parent instanceof MapContainer; + // the map might or might not contain this group + final MapContainer mapContainer = (MapContainer) parent; + final ResultHandle key = mc.invokeVirtualMethod(NI_GET_PREVIOUS_SEGMENT, keyIter); + // consume segment + mc.invokeVirtualMethod(NI_PREVIOUS, keyIter); + final ResultHandle map = mc.invokeStaticMethod(generateGetEnclosing(mapContainer, isRunTime), keyIter, + config); + // restore + mc.invokeVirtualMethod(NI_NEXT, keyIter); + final ResultHandle existing = mc.invokeInterfaceMethod(MAP_GET, map, key); + mc.ifNull(existing).falseBranch().returnValue(existing); + // add the map key and initialize the enclosed item + final MethodDescriptor ctor = accessorFinder.getConstructorFor( + MethodDescriptor.ofConstructor(member.getEnclosingDefinition().getConfigurationClass())); + final ResultHandle instance = mc.invokeStaticMethod(ctor); + final ResultHandle precedingKey = mc.invokeVirtualMethod(NI_GET_ALL_PREVIOUS_SEGMENTS, keyIter); + final ResultHandle nameBuilder = mc.newInstance(SB_NEW_STR, precedingKey); + mc.invokeStaticMethod(generateInitGroup(member.getEnclosingDefinition()), config, nameBuilder, instance); + mc.invokeInterfaceMethod(MAP_PUT, map, key, instance); + mc.returnValue(instance); + } + } + enclosingMemberMethods.put(matchNode, md); + return md; + } + + private MethodDescriptor generateGetEnclosing(final MapContainer matchNode, final boolean isRunTime) { + // name iterator cursor is placed BEFORE the map key on entry + MethodDescriptor md = enclosingMemberMethods.get(matchNode); + if (md != null) { + return md; + } + md = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, + (isRunTime ? "rt" : "si") + "GetEnclosing:" + matchNode.getCombinedName(), Object.class, + NameIterator.class, SmallRyeConfig.class); + try (MethodCreator mc = cc.getMethodCreator(md)) { + mc.setModifiers(Opcodes.ACC_STATIC); + final ResultHandle keyIter = mc.getMethodParam(0); + final ResultHandle config = mc.getMethodParam(1); + final Container parent = matchNode.getParent(); + if (parent instanceof FieldContainer) { + // get the parent + final FieldContainer fieldContainer = (FieldContainer) parent; + if (!fieldContainer.getClassMember().getPropertyName().isEmpty()) { + // consume segment + mc.invokeVirtualMethod(NI_PREVIOUS, keyIter); + } + final ResultHandle enclosing = mc.invokeStaticMethod(generateGetEnclosing(fieldContainer, isRunTime), + keyIter, config); + if (!fieldContainer.getClassMember().getPropertyName().isEmpty()) { + // restore + mc.invokeVirtualMethod(NI_NEXT, keyIter); + } + final MethodDescriptor getter = accessorFinder + .getGetterFor(fieldContainer.getClassMember().getDescriptor()); + mc.returnValue(mc.invokeStaticMethod(getter, enclosing)); + } else { + assert parent instanceof MapContainer; + // the map might or might not contain this map + final MapContainer mapContainer = (MapContainer) parent; + final ResultHandle key = mc.invokeVirtualMethod(NI_GET_PREVIOUS_SEGMENT, keyIter); + // consume enclosing map key + mc.invokeVirtualMethod(NI_PREVIOUS, keyIter); + final ResultHandle map = mc.invokeStaticMethod(generateGetEnclosing(mapContainer, isRunTime), keyIter, + config); + // restore + mc.invokeVirtualMethod(NI_NEXT, keyIter); + final ResultHandle existing = mc.invokeInterfaceMethod(MAP_GET, map, key); + mc.ifNull(existing).falseBranch().returnValue(existing); + // add the map key and initialize the enclosed item + final ResultHandle instance = mc.newInstance(TM_NEW); + mc.invokeInterfaceMethod(MAP_PUT, map, key, instance); + mc.returnValue(instance); + } + } + enclosingMemberMethods.put(matchNode, md); + return md; + } + + private FieldDescriptor getOrCreateConverterInstance(Field field) { + return getOrCreateConverterInstance(field, ConverterType.of(field)); + } + + private FieldDescriptor getOrCreateConverterInstance(Field field, ConverterType type) { + FieldDescriptor fd = convertersByType.get(type); + if (fd != null) { + return fd; + } + + fd = FieldDescriptor.of(cc.getClassName(), "conv$" + converterIndex++, Converter.class); + ResultHandle converter; + boolean storeConverter = false; + if (type instanceof Leaf) { + // simple type + final Leaf leaf = (Leaf) type; + final Class> convertWith = leaf.getConvertWith(); + if (convertWith != null) { + // TODO: temporary until type param inference is in + if (convertWith == HyphenateEnumConverter.class.asSubclass(Converter.class)) { + converter = converterSetup.newInstance(MethodDescriptor.ofConstructor(convertWith, Class.class), + converterSetup.loadClass(type.getLeafType())); + } else { + converter = converterSetup.newInstance(MethodDescriptor.ofConstructor(convertWith)); + } + } else { + final ResultHandle clazzHandle = converterSetup.loadClass(leaf.getLeafType()); + converter = converterSetup.invokeVirtualMethod(SRC_GET_CONVERTER, clinitConfig, clazzHandle); + storeConverter = true; + } + } else if (type instanceof ArrayOf) { + final ArrayOf arrayOf = (ArrayOf) type; + final ResultHandle nestedConv = instanceCache + .get(getOrCreateConverterInstance(field, arrayOf.getElementType())); + converter = converterSetup.invokeStaticMethod(CONVS_NEW_ARRAY_CONVERTER, nestedConv, + converterSetup.loadClass(arrayOf.getArrayType())); + } else if (type instanceof CollectionOf) { + final CollectionOf collectionOf = (CollectionOf) type; + final ResultHandle nestedConv = instanceCache + .get(getOrCreateConverterInstance(field, collectionOf.getElementType())); + final ResultHandle factory; + final Class collectionClass = collectionOf.getCollectionClass(); + if (collectionClass == List.class) { + factory = converterSetup.invokeStaticMethod(CU_LIST_FACTORY); + } else if (collectionClass == Set.class) { + factory = converterSetup.invokeStaticMethod(CU_SET_FACTORY); + } else if (collectionClass == SortedSet.class) { + factory = converterSetup.invokeStaticMethod(CU_SORTED_SET_FACTORY); + } else { + throw reportError(field, "Unsupported configuration collection type: %s", collectionClass); + } + converter = converterSetup.invokeStaticMethod(CONVS_NEW_COLLECTION_CONVERTER, nestedConv, factory); + } else if (type instanceof LowerBoundCheckOf) { + final LowerBoundCheckOf boundCheckOf = (LowerBoundCheckOf) type; + // todo: add in bounds checker + converter = instanceCache + .get(getOrCreateConverterInstance(field, boundCheckOf.getClassConverterType())); + } else if (type instanceof UpperBoundCheckOf) { + final UpperBoundCheckOf boundCheckOf = (UpperBoundCheckOf) type; + // todo: add in bounds checker + converter = instanceCache + .get(getOrCreateConverterInstance(field, boundCheckOf.getClassConverterType())); + } else if (type instanceof MinMaxValidated) { + MinMaxValidated minMaxValidated = (MinMaxValidated) type; + String min = minMaxValidated.getMin(); + boolean minInclusive = minMaxValidated.isMinInclusive(); + String max = minMaxValidated.getMax(); + boolean maxInclusive = minMaxValidated.isMaxInclusive(); + final ResultHandle nestedConv = instanceCache + .get(getOrCreateConverterInstance(field, minMaxValidated.getNestedType())); + if (min != null) { + if (max != null) { + converter = converterSetup.invokeStaticMethod( + CONVS_RANGE_VALUE_STRING_CONVERTER, + nestedConv, + converterSetup.load(min), + converterSetup.load(minInclusive), + converterSetup.load(max), + converterSetup.load(maxInclusive)); + } else { + converter = converterSetup.invokeStaticMethod( + CONVS_MINIMUM_VALUE_STRING_CONVERTER, + nestedConv, + converterSetup.load(min), + converterSetup.load(minInclusive)); + } + } else { + assert min == null && max != null; + converter = converterSetup.invokeStaticMethod( + CONVS_MAXIMUM_VALUE_STRING_CONVERTER, + nestedConv, + converterSetup.load(max), + converterSetup.load(maxInclusive)); + } + } else if (type instanceof OptionalOf) { + OptionalOf optionalOf = (OptionalOf) type; + final ResultHandle nestedConv = instanceCache + .get(getOrCreateConverterInstance(field, optionalOf.getNestedType())); + converter = converterSetup.invokeStaticMethod(CONVS_NEW_OPTIONAL_CONVERTER, nestedConv); + } else if (type instanceof PatternValidated) { + PatternValidated patternValidated = (PatternValidated) type; + final ResultHandle nestedConv = instanceCache + .get(getOrCreateConverterInstance(field, patternValidated.getNestedType())); + final ResultHandle patternStr = converterSetup.load(patternValidated.getPatternString()); + converter = converterSetup.invokeStaticMethod(CONVS_PATTERN_CONVERTER, nestedConv, patternStr); + } else { + throw Assert.unreachableCode(); + } + cc.getFieldCreator(fd).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + converterSetup.writeStaticField(fd, converter); + convertersByType.put(type, fd); + instanceCache.put(fd, converter); + if (storeConverter) { + convertersToRegister.put(fd, type.getLeafType()); + } + return fd; + } + + public void close() { + try { + clinit.close(); + } catch (Throwable t) { + try { + cc.close(); + } catch (Throwable t2) { + t2.addSuppressed(t); + throw t2; + } + throw t; + } + cc.close(); + } + + static final class Builder { + private ClassOutput classOutput; + private BuildTimeConfigurationReader.ReadResult buildTimeReadResult; + private Map runTimeDefaults; + private List> additionalTypes; + + Builder() { + } + + ClassOutput getClassOutput() { + return classOutput; + } + + Builder setClassOutput(final ClassOutput classOutput) { + this.classOutput = classOutput; + return this; + } + + BuildTimeConfigurationReader.ReadResult getBuildTimeReadResult() { + return buildTimeReadResult; + } + + Builder setBuildTimeReadResult(final BuildTimeConfigurationReader.ReadResult buildTimeReadResult) { + this.buildTimeReadResult = buildTimeReadResult; + return this; + } + + Map getRunTimeDefaults() { + return runTimeDefaults; + } + + Builder setRunTimeDefaults(final Map runTimeDefaults) { + this.runTimeDefaults = runTimeDefaults; + return this; + } + + List> getAdditionalTypes() { + return additionalTypes; + } + + Builder setAdditionalTypes(final List> additionalTypes) { + this.additionalTypes = additionalTypes; + return this; + } + + GenerateOperation build() { + return new GenerateOperation(this); + } + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/ClassDefinition.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/ClassDefinition.java new file mode 100644 index 0000000000000..3049c764e71a4 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/ClassDefinition.java @@ -0,0 +1,279 @@ +package io.quarkus.deployment.configuration.definition; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.wildfly.common.Assert; + +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.util.StringUtil; + +/** + * + */ +public abstract class ClassDefinition extends Definition { + private final Class configurationClass; + private final Map members; + + ClassDefinition(final Builder builder) { + super(); + final Class configurationClass = builder.configurationClass; + if (configurationClass == null) { + throw new IllegalArgumentException("No configuration class given"); + } + this.configurationClass = configurationClass; + final LinkedHashMap map = new LinkedHashMap<>(builder.members.size()); + for (Map.Entry entry : builder.members.entrySet()) { + map.put(entry.getKey(), entry.getValue().construct(this)); + } + this.members = Collections.unmodifiableMap(map); + } + + public final int getMemberCount() { + return members.size(); + } + + public final Iterable getMemberNames() { + return members.keySet(); + } + + public final Iterable getMembers() { + return members.values(); + } + + public Class getConfigurationClass() { + return configurationClass; + } + + public final ClassMember getMember(String name) { + final ClassMember member = members.get(name); + if (member == null) { + throw new IllegalArgumentException("No member named \"" + name + "\" is present on " + configurationClass); + } + return member; + } + + public static abstract class ClassMember extends Member { + public abstract ClassDefinition getEnclosingDefinition(); + + public final String getName() { + return getField().getName(); + } + + public abstract Field getField(); + + public abstract FieldDescriptor getDescriptor(); + + public abstract String getPropertyName(); + + public static abstract class Specification { + Specification() { + } + + abstract Field getField(); + + abstract ClassMember construct(ClassDefinition enclosing); + } + } + + static abstract class LeafMember extends ClassMember { + private final ClassDefinition classDefinition; + private final Field field; + private final FieldDescriptor descriptor; + private final String propertyName; + + LeafMember(final ClassDefinition classDefinition, final Field field) { + this.classDefinition = Assert.checkNotNullParam("classDefinition", classDefinition); + this.field = Assert.checkNotNullParam("field", field); + final Class declaringClass = field.getDeclaringClass(); + final Class configurationClass = classDefinition.configurationClass; + if (declaringClass != configurationClass) { + throw new IllegalArgumentException( + "Member declaring " + declaringClass + " does not match configuration " + configurationClass); + } + descriptor = FieldDescriptor.of(field); + final ConfigItem configItem = field.getAnnotation(ConfigItem.class); + String propertyName = ConfigItem.HYPHENATED_ELEMENT_NAME; + if (configItem != null) { + propertyName = configItem.name(); + if (propertyName.isEmpty()) { + throw reportError(field, "Invalid empty property name"); + } + } + if (propertyName.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) { + this.propertyName = StringUtil.hyphenate(field.getName()); + } else if (propertyName.equals(ConfigItem.ELEMENT_NAME)) { + this.propertyName = field.getName(); + } else if (propertyName.equals(ConfigItem.PARENT)) { + this.propertyName = ""; + } else { + this.propertyName = propertyName; + } + } + + public Field getField() { + return field; + } + + public FieldDescriptor getDescriptor() { + return descriptor; + } + + public ClassDefinition getEnclosingDefinition() { + return classDefinition; + } + + public String getPropertyName() { + return propertyName; + } + + public static abstract class Specification extends ClassMember.Specification { + final Field field; + + Specification(final Field field) { + this.field = Assert.checkNotNullParam("field", field); + } + + Field getField() { + return field; + } + } + } + + public static final class GroupMember extends LeafMember { + private final GroupDefinition groupDefinition; + private final boolean optional; + + GroupMember(final ClassDefinition classDefinition, final Field field, final GroupDefinition groupDefinition, + final boolean optional) { + super(classDefinition, field); + this.groupDefinition = groupDefinition; + this.optional = optional; + } + + public GroupDefinition getGroupDefinition() { + return groupDefinition; + } + + public boolean isOptional() { + return optional; + } + + public static final class Specification extends LeafMember.Specification { + private final GroupDefinition groupDefinition; + private final boolean optional; + + public Specification(final Field field, final GroupDefinition groupDefinition, final boolean optional) { + super(field); + this.groupDefinition = Assert.checkNotNullParam("groupDefinition", groupDefinition); + this.optional = optional; + } + + public boolean isOptional() { + return optional; + } + + ClassMember construct(final ClassDefinition enclosing) { + return new GroupMember(enclosing, field, groupDefinition, optional); + } + } + } + + public static final class ItemMember extends LeafMember { + private final String defaultValue; + + ItemMember(final ClassDefinition classDefinition, final Field field, final String defaultValue) { + super(classDefinition, field); + this.defaultValue = defaultValue; + } + + public String getDefaultValue() { + return defaultValue; + } + + public static final class Specification extends LeafMember.Specification { + private final String defaultValue; + + public Specification(final Field field, final String defaultValue) { + super(field); + // nullable + this.defaultValue = defaultValue; + } + + ClassMember construct(final ClassDefinition enclosing) { + return new ItemMember(enclosing, field, defaultValue); + } + } + } + + public static final class MapMember extends ClassMember { + private final ClassMember nested; + + MapMember(final ClassMember nested) { + this.nested = nested; + } + + public ClassMember getNested() { + return nested; + } + + public ClassDefinition getEnclosingDefinition() { + return nested.getEnclosingDefinition(); + } + + public Field getField() { + return nested.getField(); + } + + public FieldDescriptor getDescriptor() { + return nested.getDescriptor(); + } + + public String getPropertyName() { + return nested.getPropertyName(); + } + + public static final class Specification extends ClassMember.Specification { + private final ClassMember.Specification nested; + + public Specification(final ClassMember.Specification nested) { + this.nested = Assert.checkNotNullParam("nested", nested); + } + + Field getField() { + return nested.getField(); + } + + ClassMember construct(final ClassDefinition enclosing) { + return new MapMember(nested.construct(enclosing)); + } + } + } + + public static abstract class Builder extends Definition.Builder { + Builder() { + } + + private Class configurationClass; + private final Map members = new LinkedHashMap<>(); + + public Builder setConfigurationClass(final Class configurationClass) { + this.configurationClass = configurationClass; + return this; + } + + public Class getConfigurationClass() { + return configurationClass; + } + + public void addMember(ClassMember.Specification spec) { + Assert.checkNotNullParam("spec", spec); + members.put(spec.getField().getName(), spec); + } + + public abstract ClassDefinition build(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/Definition.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/Definition.java new file mode 100644 index 0000000000000..a6919959dd487 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/Definition.java @@ -0,0 +1,35 @@ +package io.quarkus.deployment.configuration.definition; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Parameter; + +/** + * A configuration definition. Definitions always contain links to the things they contain, but not to their own + * containers. + */ +public abstract class Definition { + Definition() { + } + + public static abstract class Builder { + Builder() { + } + + public abstract Definition build(); + } + + static IllegalArgumentException reportError(AnnotatedElement e, String msg) { + if (e instanceof Member) { + return new IllegalArgumentException(msg + " at " + e + " of " + ((Member) e).getEnclosingDefinition()); + } else if (e instanceof Parameter) { + return new IllegalArgumentException(msg + " at " + e + " of " + ((Parameter) e).getDeclaringExecutable() + " of " + + ((Parameter) e).getDeclaringExecutable().getDeclaringClass()); + } else { + return new IllegalArgumentException(msg + " at " + e); + } + } + + public static abstract class Member { + public abstract Definition getEnclosingDefinition(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/GroupDefinition.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/GroupDefinition.java new file mode 100644 index 0000000000000..549e4f49dedba --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/GroupDefinition.java @@ -0,0 +1,19 @@ +package io.quarkus.deployment.configuration.definition; + +/** + * + */ +public final class GroupDefinition extends ClassDefinition { + GroupDefinition(final Builder builder) { + super(builder); + } + + public static final class Builder extends ClassDefinition.Builder { + public Builder() { + } + + public GroupDefinition build() { + return new GroupDefinition(this); + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/RootDefinition.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/RootDefinition.java new file mode 100644 index 0000000000000..3fc15bf991d7a --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/definition/RootDefinition.java @@ -0,0 +1,108 @@ +package io.quarkus.deployment.configuration.definition; + +import static io.quarkus.deployment.configuration.RunTimeConfigurationGenerator.CONFIG_CLASS_NAME; +import static io.quarkus.runtime.util.StringUtil.camelHumpsIterator; +import static io.quarkus.runtime.util.StringUtil.lowerCase; +import static io.quarkus.runtime.util.StringUtil.lowerCaseFirst; +import static io.quarkus.runtime.util.StringUtil.toList; +import static io.quarkus.runtime.util.StringUtil.withoutSuffix; + +import java.util.List; + +import org.wildfly.common.Assert; + +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; + +/** + * + */ +public final class RootDefinition extends ClassDefinition { + private final ConfigPhase configPhase; + private final String rootName; + private final FieldDescriptor descriptor; + + RootDefinition(final Builder builder) { + super(builder); + this.configPhase = builder.configPhase; + String rootName = builder.rootName; + final Class configClass = getConfigurationClass(); + final List segments = toList(camelHumpsIterator(configClass.getSimpleName())); + final List trimmedSegments; + if (configPhase == ConfigPhase.RUN_TIME) { + trimmedSegments = withoutSuffix( + withoutSuffix( + withoutSuffix( + withoutSuffix( + segments, + "Run", "Time", "Configuration"), + "Run", "Time", "Config"), + "Configuration"), + "Config"); + } else { + trimmedSegments = withoutSuffix( + withoutSuffix( + withoutSuffix( + withoutSuffix( + segments, + "Build", "Time", "Configuration"), + "Build", "Time", "Config"), + "Configuration"), + "Config"); + } + if (rootName.equals(ConfigItem.PARENT)) { + rootName = ""; + } else if (rootName.equals(ConfigItem.ELEMENT_NAME)) { + rootName = String.join("", (Iterable) () -> lowerCaseFirst(trimmedSegments.iterator())); + } else if (rootName.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) { + rootName = String.join("-", (Iterable) () -> lowerCase(trimmedSegments.iterator())); + } + this.rootName = rootName; + this.descriptor = FieldDescriptor.of(CONFIG_CLASS_NAME, String.join("", segments), Object.class); + } + + public ConfigPhase getConfigPhase() { + return configPhase; + } + + public String getRootName() { + return rootName; + } + + public FieldDescriptor getDescriptor() { + return descriptor; + } + + public static final class Builder extends ClassDefinition.Builder { + private ConfigPhase configPhase = ConfigPhase.BUILD_TIME; + private String rootName = ConfigItem.HYPHENATED_ELEMENT_NAME; + + public Builder() { + } + + public ConfigPhase getConfigPhase() { + return configPhase; + } + + public Builder setConfigPhase(final ConfigPhase configPhase) { + Assert.checkNotNullParam("configPhase", configPhase); + this.configPhase = configPhase; + return this; + } + + public String getRootName() { + return rootName; + } + + public Builder setRootName(final String rootName) { + Assert.checkNotNullParam("rootName", rootName); + this.rootName = rootName; + return this; + } + + public RootDefinition build() { + return new RootDefinition(this); + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigPatternMap.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/ConfigPatternMap.java similarity index 62% rename from core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigPatternMap.java rename to core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/ConfigPatternMap.java index e2ef97f22b4dd..fbf525fc1c6bf 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigPatternMap.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/ConfigPatternMap.java @@ -1,9 +1,10 @@ -package io.quarkus.deployment.configuration; +package io.quarkus.deployment.configuration.matching; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Objects; import java.util.TreeMap; +import java.util.function.BiFunction; import org.wildfly.common.Assert; @@ -127,6 +128,89 @@ public void addChild(final String childName, final ConfigPatternMap child) { children.put(childName, child); } + public static ConfigPatternMap merge(final ConfigPatternMap param0, + final ConfigPatternMap param1, final BiFunction combinator) { + final ConfigPatternMap result = new ConfigPatternMap<>(); + final T matched0 = param0.getMatched(); + final U matched1 = param1.getMatched(); + result.setMatched(combinator.apply(matched0, matched1)); + + // they're sorted; combine them in order + final Iterator iter0 = param0.childNames().iterator(); + final Iterator iter1 = param1.childNames().iterator(); + String next0; + String next1; + if (iter0.hasNext() && iter1.hasNext()) { + next0 = iter0.next(); + next1 = iter1.next(); + for (;;) { + if (next0.compareTo(next1) < 0) { + result.addChild(next0, merge0(param0.getChild(next0), combinator)); + if (iter0.hasNext()) { + next0 = iter0.next(); + } else { + result.addChild(next1, merge1(param1.getChild(next1), combinator)); + break; + } + } else if (next0.compareTo(next1) > 0) { + result.addChild(next1, merge1(param1.getChild(next1), combinator)); + if (iter1.hasNext()) { + next1 = iter1.next(); + } else { + result.addChild(next0, merge0(param0.getChild(next0), combinator)); + break; + } + } else { + assert next0.compareTo(next1) == 0; + result.addChild(next0, merge(param0.getChild(next0), param1.getChild(next1), combinator)); + if (iter0.hasNext() && iter1.hasNext()) { + next0 = iter0.next(); + next1 = iter1.next(); + } else { + break; + } + } + } + } + while (iter0.hasNext()) { + next0 = iter0.next(); + result.addChild(next0, merge0(param0.getChild(next0), combinator)); + } + while (iter1.hasNext()) { + next1 = iter1.next(); + result.addChild(next1, merge1(param1.getChild(next1), combinator)); + } + return result; + } + + private static ConfigPatternMap merge0(final ConfigPatternMap param0, + final BiFunction combinator) { + final ConfigPatternMap result = new ConfigPatternMap<>(); + final T matched0 = param0.getMatched(); + result.setMatched(combinator.apply(matched0, null)); + final Iterator iter0 = param0.childNames().iterator(); + String next0; + while (iter0.hasNext()) { + next0 = iter0.next(); + result.addChild(next0, merge0(param0.getChild(next0), combinator)); + } + return result; + } + + private static ConfigPatternMap merge1(final ConfigPatternMap param1, + final BiFunction combinator) { + final ConfigPatternMap result = new ConfigPatternMap<>(); + final U matched1 = param1.getMatched(); + result.setMatched(combinator.apply(null, matched1)); + final Iterator iter1 = param1.childNames().iterator(); + String next1; + while (iter1.hasNext()) { + next1 = iter1.next(); + result.addChild(next1, merge1(param1.getChild(next1), combinator)); + } + return result; + } + public static class PatternIterator implements Iterator { ConfigPatternMap current; ConfigPatternMap next; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/Container.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/Container.java new file mode 100644 index 0000000000000..b1e87d7ae7770 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/Container.java @@ -0,0 +1,63 @@ +package io.quarkus.deployment.configuration.matching; + +import java.lang.reflect.Field; + +import io.quarkus.deployment.configuration.definition.ClassDefinition; + +/** + * A container for a configuration key path. + */ +public abstract class Container { + Container() { + } + + /** + * Get the parent container, or {@code null} if the container is a root. Presently only + * field containers may be roots. + * + * @return the parent container + */ + public abstract Container getParent(); + + /** + * Find the field that will ultimately hold this value. + * + * @return the field (must not be {@code null}) + */ + public final Field findField() { + return getClassMember().getField(); + } + + /** + * Find the enclosing class definition that will ultimately hold this value. + * + * @return the class definition (must not be {@code null}) + */ + public final ClassDefinition findEnclosingClass() { + return getClassMember().getEnclosingDefinition(); + } + + /** + * Find the enclosing class member. + * + * @return the enclosing class member + */ + public abstract ClassDefinition.ClassMember getClassMember(); + + /** + * Get the combined name of this item. + * + * @return the combined name (must not be {@code null}) + */ + public final String getCombinedName() { + return getCombinedName(new StringBuilder()).toString(); + } + + abstract StringBuilder getCombinedName(StringBuilder sb); + + public final String getPropertyName() { + return getPropertyName(new StringBuilder()).toString(); + } + + abstract StringBuilder getPropertyName(StringBuilder sb); +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/FieldContainer.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/FieldContainer.java new file mode 100644 index 0000000000000..cd97ddfc8c9f8 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/FieldContainer.java @@ -0,0 +1,70 @@ +package io.quarkus.deployment.configuration.matching; + +import org.wildfly.common.Assert; + +import io.quarkus.deployment.configuration.definition.ClassDefinition; +import io.quarkus.deployment.configuration.definition.RootDefinition; + +/** + * + */ +public final class FieldContainer extends Container { + private final Container parent; + private final ClassDefinition.ClassMember member; + + public FieldContainer(final Container parent, final ClassDefinition.ClassMember member) { + this.parent = parent; + this.member = Assert.checkNotNullParam("member", member); + } + + public Container getParent() { + return parent; + } + + public ClassDefinition.ClassMember getClassMember() { + return member; + } + + StringBuilder getCombinedName(final StringBuilder sb) { + Container parent = getParent(); + if (parent != null) { + parent.getCombinedName(sb); + } + final ClassDefinition enclosing = member.getEnclosingDefinition(); + if (enclosing instanceof RootDefinition) { + RootDefinition rootDefinition = (RootDefinition) enclosing; + String rootName = rootDefinition.getRootName(); + if (!rootName.isEmpty()) { + sb.append(rootName.replace('.', ':')); + } + } + if (sb.length() > 0) { + sb.append(':'); + } + sb.append(member.getName()); + return sb; + } + + StringBuilder getPropertyName(final StringBuilder sb) { + Container parent = getParent(); + if (parent != null) { + parent.getPropertyName(sb); + } + final ClassDefinition enclosing = member.getEnclosingDefinition(); + if (enclosing instanceof RootDefinition) { + RootDefinition rootDefinition = (RootDefinition) enclosing; + String rootName = rootDefinition.getRootName(); + if (!rootName.isEmpty()) { + sb.append(rootName); + } + } + final String propertyName = member.getPropertyName(); + if (!propertyName.isEmpty()) { + if (sb.length() > 0) { + sb.append('.'); + } + sb.append(propertyName); + } + return sb; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/MapContainer.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/MapContainer.java new file mode 100644 index 0000000000000..9360e81635e07 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/MapContainer.java @@ -0,0 +1,46 @@ +package io.quarkus.deployment.configuration.matching; + +import org.wildfly.common.Assert; + +import io.quarkus.deployment.configuration.definition.ClassDefinition; + +/** + * A map container. + */ +public final class MapContainer extends Container { + private final Container parent; + private final ClassDefinition.ClassMember mapMember; + + public MapContainer(final Container parent, final ClassDefinition.ClassMember mapMember) { + this.parent = Assert.checkNotNullParam("parent", parent); + this.mapMember = mapMember; + } + + public ClassDefinition.ClassMember getClassMember() { + return mapMember; + } + + public Container getParent() { + return parent; + } + + StringBuilder getCombinedName(final StringBuilder sb) { + // maps always have a parent + getParent().getCombinedName(sb); + if (sb.length() > 0) { + sb.append(':'); + } + sb.append('*'); + return sb; + } + + StringBuilder getPropertyName(final StringBuilder sb) { + // maps always have a parent + getParent().getPropertyName(sb); + if (sb.length() > 0) { + sb.append('.'); + } + sb.append('*'); + return sb; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/PatternMapBuilder.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/PatternMapBuilder.java new file mode 100644 index 0000000000000..d614f7a49caae --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/matching/PatternMapBuilder.java @@ -0,0 +1,88 @@ +package io.quarkus.deployment.configuration.matching; + +import java.util.List; + +import io.quarkus.deployment.configuration.definition.ClassDefinition; +import io.quarkus.deployment.configuration.definition.RootDefinition; +import io.quarkus.runtime.configuration.NameIterator; + +/** + * + */ +public final class PatternMapBuilder { + + private PatternMapBuilder() { + } + + public static ConfigPatternMap makePatterns(List rootDefinitions) { + ConfigPatternMap patternMap = new ConfigPatternMap<>(); + for (RootDefinition rootDefinition : rootDefinitions) { + final String rootName = rootDefinition.getRootName(); + ConfigPatternMap addTo = patternMap, child; + if (!rootName.isEmpty()) { + NameIterator ni = new NameIterator(rootName); + assert ni.hasNext(); + do { + final String seg = ni.getNextSegment(); + child = addTo.getChild(seg); + ni.next(); + if (child == null) { + addTo.addChild(seg, child = new ConfigPatternMap<>()); + } + addTo = child; + } while (ni.hasNext()); + } + addGroup(addTo, rootDefinition, null); + } + return patternMap; + } + + private static void addGroup(ConfigPatternMap patternMap, ClassDefinition current, + Container parent) { + for (ClassDefinition.ClassMember member : current.getMembers()) { + final String propertyName = member.getPropertyName(); + ConfigPatternMap addTo = patternMap; + FieldContainer newNode; + if (!propertyName.isEmpty()) { + NameIterator ni = new NameIterator(propertyName); + assert ni.hasNext(); + do { + final String seg = ni.getNextSegment(); + ConfigPatternMap child = addTo.getChild(seg); + if (child == null) { + addTo.addChild(seg, child = new ConfigPatternMap<>()); + } + addTo = child; + ni.next(); + } while (ni.hasNext()); + } + newNode = new FieldContainer(parent, member); + addMember(addTo, member, newNode); + } + } + + private static void addMember(ConfigPatternMap patternMap, ClassDefinition.ClassMember member, + Container container) { + if (member instanceof ClassDefinition.ItemMember) { + Container matched = patternMap.getMatched(); + if (matched != null) { + throw new IllegalArgumentException( + "Multiple matching properties for name \"" + matched.getPropertyName() + + "\" property was matched by both " + container.findField() + " and " + matched.findField() + + ". This is likely because you have an incompatible combination of extensions that both define the same properties (e.g. including both reactive and blocking database extensions)"); + } + patternMap.setMatched(container); + } else if (member instanceof ClassDefinition.MapMember) { + ClassDefinition.MapMember mapMember = (ClassDefinition.MapMember) member; + ConfigPatternMap addTo = patternMap.getChild(ConfigPatternMap.WILD_CARD); + if (addTo == null) { + patternMap.addChild(ConfigPatternMap.WILD_CARD, addTo = new ConfigPatternMap<>()); + } + final ClassDefinition.ClassMember nestedMember = mapMember.getNested(); + addMember(addTo, nestedMember, new MapContainer(container, nestedMember)); + } else { + assert member instanceof ClassDefinition.GroupMember; + addGroup(patternMap, ((ClassDefinition.GroupMember) member).getGroupDefinition(), container); + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/ArrayOf.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/ArrayOf.java new file mode 100644 index 0000000000000..8116feeb95576 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/ArrayOf.java @@ -0,0 +1,56 @@ +package io.quarkus.deployment.configuration.type; + +import java.lang.reflect.Array; +import java.util.Objects; + +/** + * + */ +public final class ArrayOf extends ConverterType { + private final ConverterType type; + private int hashCode; + private Class arrayType; + + public ArrayOf(final ConverterType type) { + this.type = type; + } + + public ConverterType getElementType() { + return type; + } + + @Override + public Class getLeafType() { + return type.getLeafType(); + } + + public Class getArrayType() { + Class arrayType = this.arrayType; + if (arrayType == null) { + this.arrayType = arrayType = Array.newInstance(getLeafType(), 0).getClass(); + } + return arrayType; + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(type, ArrayOf.class); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof ArrayOf && equals((ArrayOf) obj); + } + + public boolean equals(final ArrayOf obj) { + return this == obj || obj != null && type.equals(obj.type); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/CollectionOf.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/CollectionOf.java new file mode 100644 index 0000000000000..3be6c14499eba --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/CollectionOf.java @@ -0,0 +1,52 @@ +package io.quarkus.deployment.configuration.type; + +import java.util.Objects; + +/** + * + */ +public final class CollectionOf extends ConverterType { + private final ConverterType type; + private final Class collectionClass; + private int hashCode; + + public CollectionOf(final ConverterType type, final Class collectionClass) { + this.type = type; + this.collectionClass = collectionClass; + } + + public ConverterType getElementType() { + return type; + } + + @Override + public Class getLeafType() { + return type.getLeafType(); + } + + public Class getCollectionClass() { + return collectionClass; + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(type, collectionClass, CollectionOf.class); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof CollectionOf && equals((CollectionOf) obj); + } + + public boolean equals(final CollectionOf obj) { + return this == obj || obj != null && type.equals(obj.type); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/ConverterType.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/ConverterType.java new file mode 100644 index 0000000000000..15805bf22e0ab --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/ConverterType.java @@ -0,0 +1,112 @@ +package io.quarkus.deployment.configuration.type; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; + +import io.quarkus.deployment.util.ReflectUtil; +import io.quarkus.runtime.annotations.ConvertWith; +import io.quarkus.runtime.annotations.DefaultConverter; +import io.quarkus.runtime.configuration.HyphenateEnumConverter; + +/** + * + */ +public abstract class ConverterType { + ConverterType() { + } + + public abstract Class getLeafType(); + + public static ConverterType of(Field member) { + return of(member.getGenericType(), member); + } + + public static ConverterType of(Parameter parameter) { + return of(parameter.getParameterizedType(), parameter); + } + + public static ConverterType of(Type type, AnnotatedElement element) { + if (type instanceof GenericArrayType) { + GenericArrayType genericArrayType = (GenericArrayType) type; + return new ArrayOf(of(genericArrayType.getGenericComponentType(), element)); + } else if (type instanceof Class) { + // simple type + Class clazz = (Class) type; + if (clazz.isArray()) { + return new ArrayOf(of(clazz.getComponentType(), element)); + } + ConvertWith convertWith = element.getAnnotation(ConvertWith.class); + Leaf leaf; + if (convertWith == null && element.getAnnotation(DefaultConverter.class) == null && clazz.isEnum()) { + // use our hyphenated converter by default + leaf = new Leaf(clazz, HyphenateEnumConverter.class); + } else { + leaf = new Leaf(clazz, convertWith == null ? null : convertWith.value()); + } + // vvv todo: add validations here vvv + // return result + return leaf; + } else if (type instanceof ParameterizedType) { + final ParameterizedType paramType = (ParameterizedType) type; + final Class rawType = ReflectUtil.rawTypeOf(paramType); + final Type[] args = paramType.getActualTypeArguments(); + if (args.length == 1) { + final Type arg = args[0]; + if (rawType == Class.class) { + ConverterType result = of(Class.class, element); + if (arg instanceof WildcardType) { + final WildcardType wcType = (WildcardType) arg; + // gather bounds for validation + Class[] upperBounds = ReflectUtil.rawTypesOfDestructive(wcType.getUpperBounds()); + Class[] lowerBounds = ReflectUtil.rawTypesOfDestructive(wcType.getLowerBounds()); + for (Class upperBound : upperBounds) { + if (upperBound != Object.class) { + result = new UpperBoundCheckOf(upperBound, result); + } + } + for (Class lowerBound : lowerBounds) { + result = new LowerBoundCheckOf(lowerBound, result); + } + return result; + } + throw new IllegalArgumentException("Class configuration item types cannot be invariant"); + } + final ConverterType nested = of(arg, element); + if (rawType == List.class || rawType == Set.class || rawType == SortedSet.class + || rawType == NavigableSet.class) { + return new CollectionOf(nested, rawType); + } else if (rawType == Optional.class) { + return new OptionalOf(nested); + } else { + throw unsupportedType(type); + } + } else if (args.length == 2) { + if (rawType == Map.class) { + // the real converter is the converter for the value type + return of(ReflectUtil.typeOfParameter(paramType, 1), element); + } else { + throw unsupportedType(type); + } + } else { + throw unsupportedType(type); + } + } else { + throw unsupportedType(type); + } + } + + private static IllegalArgumentException unsupportedType(final Type type) { + return new IllegalArgumentException("Unsupported type: " + type); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/Leaf.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/Leaf.java new file mode 100644 index 0000000000000..827d6cdf52372 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/Leaf.java @@ -0,0 +1,50 @@ +package io.quarkus.deployment.configuration.type; + +import java.util.Objects; + +import org.eclipse.microprofile.config.spi.Converter; + +/** + * + */ +public final class Leaf extends ConverterType { + private final Class type; + private final Class> convertWith; + private int hashCode; + + public Leaf(final Class type, final Class convertWith) { + this.type = type; + this.convertWith = (Class>) convertWith; + } + + @Override + public Class getLeafType() { + return type; + } + + public Class> getConvertWith() { + return convertWith; + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(type, convertWith); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof Leaf && equals((Leaf) obj); + } + + public boolean equals(final Leaf obj) { + return obj == this || obj != null && type == obj.type && convertWith == obj.convertWith; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/LowerBoundCheckOf.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/LowerBoundCheckOf.java new file mode 100644 index 0000000000000..29bb7430f2b6b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/LowerBoundCheckOf.java @@ -0,0 +1,52 @@ +package io.quarkus.deployment.configuration.type; + +import java.util.Objects; + +/** + * + */ +public final class LowerBoundCheckOf extends ConverterType { + private final Class lowerBound; + private final ConverterType classConverterType; + private int hashCode; + + public LowerBoundCheckOf(final Class lowerBound, final ConverterType classConverterType) { + this.lowerBound = lowerBound; + this.classConverterType = classConverterType; + } + + public Class getLowerBound() { + return lowerBound; + } + + public ConverterType getClassConverterType() { + return classConverterType; + } + + @Override + public Class getLeafType() { + return classConverterType.getLeafType(); + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(classConverterType, lowerBound, LowerBoundCheckOf.class); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof LowerBoundCheckOf && equals((LowerBoundCheckOf) obj); + } + + public boolean equals(final LowerBoundCheckOf obj) { + return obj == this || obj != null && lowerBound == obj.lowerBound && classConverterType.equals(obj.classConverterType); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/MinMaxValidated.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/MinMaxValidated.java new file mode 100644 index 0000000000000..b96153bf7fe71 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/MinMaxValidated.java @@ -0,0 +1,72 @@ +package io.quarkus.deployment.configuration.type; + +import java.util.Objects; + +/** + * + */ +public final class MinMaxValidated extends ConverterType { + private final ConverterType type; + private final String min; + private final boolean minInclusive; + private final String max; + private final boolean maxInclusive; + private int hashCode; + + public MinMaxValidated(final ConverterType type, final String min, final boolean minInclusive, final String max, + final boolean maxInclusive) { + this.type = type; + this.min = min; + this.minInclusive = minInclusive; + this.max = max; + this.maxInclusive = maxInclusive; + } + + public ConverterType getNestedType() { + return type; + } + + public String getMin() { + return min; + } + + public boolean isMinInclusive() { + return minInclusive; + } + + public String getMax() { + return max; + } + + public boolean isMaxInclusive() { + return maxInclusive; + } + + @Override + public Class getLeafType() { + return type.getLeafType(); + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(type, min, Boolean.valueOf(minInclusive), max, Boolean.valueOf(maxInclusive)); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof MinMaxValidated && equals((MinMaxValidated) obj); + } + + public boolean equals(final MinMaxValidated obj) { + return this == obj || obj != null && Objects.equals(type, obj.type) && Objects.equals(min, obj.min) + && Objects.equals(max, obj.max) && maxInclusive == obj.maxInclusive && minInclusive == obj.minInclusive; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/OptionalOf.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/OptionalOf.java new file mode 100644 index 0000000000000..8ec697235bc0f --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/OptionalOf.java @@ -0,0 +1,45 @@ +package io.quarkus.deployment.configuration.type; + +import java.util.Objects; + +/** + * + */ +public final class OptionalOf extends ConverterType { + private final ConverterType type; + private int hashCode; + + public OptionalOf(final ConverterType type) { + this.type = type; + } + + public ConverterType getNestedType() { + return type; + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(type, OptionalOf.class); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof OptionalOf && equals((OptionalOf) obj); + } + + public boolean equals(final OptionalOf obj) { + return this == obj || obj != null && type.equals(obj.type); + } + + public Class getLeafType() { + return type.getLeafType(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/PatternValidated.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/PatternValidated.java new file mode 100644 index 0000000000000..b41312458779d --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/PatternValidated.java @@ -0,0 +1,52 @@ +package io.quarkus.deployment.configuration.type; + +import java.util.Objects; + +/** + * + */ +public final class PatternValidated extends ConverterType { + private final ConverterType type; + private final String patternString; + private int hashCode; + + public PatternValidated(final ConverterType type, final String patternString) { + this.type = type; + this.patternString = patternString; + } + + public ConverterType getNestedType() { + return type; + } + + public String getPatternString() { + return patternString; + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(type, patternString); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof PatternValidated && equals((PatternValidated) obj); + } + + public boolean equals(final PatternValidated obj) { + return obj == this || obj != null && type.equals(obj.type) && patternString.equals(obj.patternString); + } + + @Override + public Class getLeafType() { + return type.getLeafType(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/UpperBoundCheckOf.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/UpperBoundCheckOf.java new file mode 100644 index 0000000000000..f9aaca6a4d73e --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/type/UpperBoundCheckOf.java @@ -0,0 +1,52 @@ +package io.quarkus.deployment.configuration.type; + +import java.util.Objects; + +/** + * + */ +public final class UpperBoundCheckOf extends ConverterType { + private final Class upperBound; + private final ConverterType classConverterType; + private int hashCode; + + public UpperBoundCheckOf(final Class upperBound, final ConverterType classConverterType) { + this.upperBound = upperBound; + this.classConverterType = classConverterType; + } + + public Class getUpperBound() { + return upperBound; + } + + public ConverterType getClassConverterType() { + return classConverterType; + } + + @Override + public Class getLeafType() { + return classConverterType.getLeafType(); + } + + @Override + public int hashCode() { + int hashCode = this.hashCode; + if (hashCode == 0) { + hashCode = Objects.hash(classConverterType, upperBound, UpperBoundCheckOf.class); + if (hashCode == 0) { + hashCode = 0x8000_0000; + } + this.hashCode = hashCode; + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof UpperBoundCheckOf && equals((UpperBoundCheckOf) obj); + } + + public boolean equals(final UpperBoundCheckOf obj) { + return obj == this || obj != null && upperBound == obj.upperBound && classConverterType.equals(obj.classConverterType); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java index e4c79e20ea409..5d38c35198fa2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java @@ -72,7 +72,7 @@ static final class IndexDependencyConfiguration { void addConfiguredIndexedDependencies(BuildProducer indexDependencyBuildItemBuildProducer) { for (IndexDependencyConfig indexDependencyConfig : config.indexDependency.values()) { indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem(indexDependencyConfig.groupId, - indexDependencyConfig.artifactId, indexDependencyConfig.classifier)); + indexDependencyConfig.artifactId, indexDependencyConfig.classifier.orElse(null))); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexDependencyConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexDependencyConfig.java index bb8c39099d7a3..418dad5d7ba4b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexDependencyConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexDependencyConfig.java @@ -1,5 +1,7 @@ package io.quarkus.deployment.index; +import java.util.Optional; + import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -22,6 +24,6 @@ public class IndexDependencyConfig { * The maven classifier of the artifact to index */ @ConfigItem - String classifier; + Optional classifier; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index 46cafcd65e7ad..bc90baeec0057 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -1,7 +1,9 @@ package io.quarkus.deployment.logging; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; +import java.util.logging.Handler; import java.util.stream.Collectors; import org.jboss.logmanager.EmbeddedConfigurator; @@ -11,11 +13,14 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.LogCategoryBuildItem; +import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.runtime.logging.LogConfig; import io.quarkus.runtime.logging.LoggingSetupRecorder; @@ -73,8 +78,12 @@ void miscSetup( @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - void setupLoggingRuntimeInit(LoggingSetupRecorder recorder, LogConfig log) { - recorder.initializeLogging(log); + void setupLoggingRuntimeInit(LoggingSetupRecorder recorder, LogConfig log, List handlers, + List consoleFormatItems) { + final List>> list = handlers.stream().map(LogHandlerBuildItem::getHandlerValue) + .collect(Collectors.toList()); + recorder.initializeLogging(log, list, + consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue).collect(Collectors.toList())); } @BuildStep diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java index ecc33b77a2b89..df72dd3f5401a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java @@ -1,7 +1,6 @@ package io.quarkus.deployment.pkg; import java.io.File; -import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -16,7 +15,7 @@ public class NativeConfig { * Additional arguments to pass to the build process */ @ConfigItem - public List additionalBuildArgs; + public Optional> additionalBuildArgs; /** * If the HTTP url handler should be enabled, allowing you to do URL.openConnection() for HTTP URLs @@ -52,7 +51,7 @@ public class NativeConfig { * The location of the Graal distribution */ @ConfigItem(defaultValue = "${GRAALVM_HOME:}") - public String graalvmHome; + public Optional graalvmHome; /** * The location of the JDK @@ -141,13 +140,13 @@ public class NativeConfig { * a container build is always done. */ @ConfigItem - public String containerRuntime = ""; + public Optional containerRuntime; /** * Options to pass to the container runtime */ @ConfigItem - public List containerRuntimeOptions = new ArrayList<>(); + public Optional> containerRuntimeOptions; /** * If the resulting image should allow VM introspection diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index c98f1a080e7a8..9efe7e622399b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -1,6 +1,7 @@ package io.quarkus.deployment.pkg; import java.util.List; +import java.util.Optional; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -41,11 +42,24 @@ public class PackageConfig { * Files that should not be copied to the output artifact */ @ConfigItem - public List userConfiguredIgnoredEntries; + public Optional> userConfiguredIgnoredEntries; /** * The suffix that is applied to the runner jar and native images */ @ConfigItem(defaultValue = "-runner") public String runnerSuffix; + + /** + * The output folder in which to place the output, this is resolved relative to the build + * systems target directory. + */ + @ConfigItem + public Optional outputDirectory; + + /** + * The name of the final artifact + */ + @ConfigItem + public Optional outputName; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/BuildSystemTargetBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/BuildSystemTargetBuildItem.java new file mode 100644 index 0000000000000..89bed8dd9461e --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/BuildSystemTargetBuildItem.java @@ -0,0 +1,27 @@ +package io.quarkus.deployment.pkg.builditem; + +import java.nio.file.Path; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * The build systems target directory. This is used to produce {@link OutputTargetBuildItem} + */ +public final class BuildSystemTargetBuildItem extends SimpleBuildItem { + + private final Path outputDirectory; + private final String baseName; + + public BuildSystemTargetBuildItem(Path outputDirectory, String baseName) { + this.outputDirectory = outputDirectory; + this.baseName = baseName; + } + + public Path getOutputDirectory() { + return outputDirectory; + } + + public String getBaseName() { + return baseName; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/ErrorReplacingProcessReader.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/ErrorReplacingProcessReader.java index 5bf7b55422fbd..d013fef09ac93 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/ErrorReplacingProcessReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/ErrorReplacingProcessReader.java @@ -110,9 +110,9 @@ private void handleErrorState(File report, String firstLine, Deque queue try { String fullName = m.group(1); - int idex = fullName.lastIndexOf('.'); - String clazz = fullName.substring(0, idex); - String method = fullName.substring(idex + 1); + int index = fullName.lastIndexOf('.'); + String clazz = fullName.substring(0, index); + String method = fullName.substring(index + 1); if (reportAnalyzer == null) { reportAnalyzer = new ReportAnalyzer(report.getAbsolutePath()); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index 958b3021905ea..e063859f2461d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -61,6 +61,7 @@ import io.quarkus.deployment.builditem.TransformedClassesBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageSourceJarBuildItem; @@ -115,6 +116,15 @@ public class JarResultBuildStep { // makes a subsequent uberJar creation fail in java 8 (but works fine in Java 11) private static final OpenOption[] DEFAULT_OPEN_OPTIONS = { TRUNCATE_EXISTING, WRITE, CREATE }; + @BuildStep + OutputTargetBuildItem outputTarget(BuildSystemTargetBuildItem bst, PackageConfig packageConfig) { + String name = packageConfig.outputName.isPresent() ? packageConfig.outputName.get() : bst.getBaseName(); + Path path = packageConfig.outputDirectory.isPresent() + ? bst.getOutputDirectory().resolve(packageConfig.outputDirectory.get()) + : bst.getOutputDirectory(); + return new OutputTargetBuildItem(path, name); + } + @BuildStep(onlyIf = JarRequired.class) ArtifactResultBuildItem jarOutput(JarBuildItem jarBuildItem) { if (jarBuildItem.getLibraryDir() != null) { @@ -180,7 +190,7 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, final StringBuilder classPath = new StringBuilder(); final Map> services = new HashMap<>(); Set finalIgnoredEntries = new HashSet<>(IGNORED_ENTRIES); - finalIgnoredEntries.addAll(packageConfig.userConfiguredIgnoredEntries); + packageConfig.userConfiguredIgnoredEntries.ifPresent(finalIgnoredEntries::addAll); final List appDeps = curateOutcomeBuildItem.getEffectiveModel().getUserDependencies(); @@ -567,6 +577,7 @@ private void generateManifest(FileSystem runnerZipFs, final String classPath, Pa } else { Files.createDirectories(runnerZipFs.getPath("META-INF")); } + Files.createDirectories(manifestPath.getParent()); Attributes attributes = manifest.getMainAttributes(); attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); if (attributes.containsKey(Attributes.Name.CLASS_PATH)) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 00e4ee8d224a5..5bc783d7f4e2e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -31,6 +32,7 @@ import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageSourceJarBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.util.FileUtil; public class NativeImageBuildStep { @@ -72,19 +74,21 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa final String runnerJarName = runnerJar.getFileName().toString(); - boolean vmVersionOutOfDate = isThisGraalVMVersionObsolete(); - HashMap env = new HashMap<>(System.getenv()); List nativeImage; String noPIE = ""; - if (!"".equals(nativeConfig.containerRuntime) || nativeConfig.containerBuild) { - String containerRuntime = nativeConfig.containerRuntime.isEmpty() ? "docker" : nativeConfig.containerRuntime; + if (nativeConfig.containerRuntime.isPresent() || nativeConfig.containerBuild) { + String containerRuntime = nativeConfig.containerRuntime.orElse("docker"); // E.g. "/usr/bin/docker run -v {{PROJECT_DIR}}:/project --rm quarkus/graalvm-native-image" nativeImage = new ArrayList<>(); - Collections.addAll(nativeImage, containerRuntime, "run", "-v", - outputDir.toAbsolutePath() + ":/project:z"); + + String outputPath = outputDir.toAbsolutePath().toString(); + if (IS_WINDOWS) { + outputPath = FileUtil.translateToVolumePath(outputPath); + } + Collections.addAll(nativeImage, containerRuntime, "run", "-v", outputPath + ":/project:z"); if (IS_LINUX) { if ("docker".equals(containerRuntime)) { @@ -98,7 +102,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa nativeImage.add("--userns=keep-id"); } } - nativeImage.addAll(nativeConfig.containerRuntimeOptions); + nativeConfig.containerRuntimeOptions.ifPresent(nativeImage::addAll); if (nativeConfig.debugBuildProcess && nativeConfig.publishDebugBuildProcessPort) { // publish the debug port onto the host if asked for nativeImage.add("--publish=" + DEBUG_BUILD_PROCESS_PORT + ":" + DEBUG_BUILD_PROCESS_PORT); @@ -109,12 +113,10 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa noPIE = detectNoPIE(); } - String graal = nativeConfig.graalvmHome; + Optional graal = nativeConfig.graalvmHome; File java = nativeConfig.javaHome; - if (graal != null) { - env.put(GRAALVM_HOME, graal); - } else { - graal = env.get(GRAALVM_HOME); + if (graal.isPresent()) { + env.put(GRAALVM_HOME, graal.get()); } if (java == null) { // try system property first - it will be the JAVA_HOME used by the current JVM @@ -132,6 +134,29 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa nativeImage = Collections.singletonList(getNativeImageExecutable(graal, java, env).getAbsolutePath()); } + final Optional graalVMVersion; + + try { + List versionCommand = new ArrayList<>(nativeImage); + versionCommand.add("--version"); + + Process versionProcess = new ProcessBuilder(versionCommand.toArray(new String[0])) + .redirectErrorStream(true) + .start(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(versionProcess.getInputStream(), StandardCharsets.UTF_8))) { + graalVMVersion = reader.lines().filter((l) -> l.startsWith("GraalVM Version")).findFirst(); + } + } catch (Exception e) { + throw new RuntimeException("Failed to get GraalVM version", e); + } + + if (graalVMVersion.isPresent()) { + checkGraalVMVersion(graalVMVersion.get()); + } else { + log.error("Unable to get GraalVM version from the native-image binary."); + } + try { List command = new ArrayList<>(nativeImage); if (nativeConfig.cleanupServer) { @@ -146,6 +171,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa process.waitFor(); } Boolean enableSslNative = false; + boolean enableAllTimeZones = false; for (NativeImageSystemPropertyBuildItem prop : nativeImageProperties) { //todo: this should be specific build items if (prop.getKey().equals("quarkus.ssl.native") && prop.getValue() != null) { @@ -156,6 +182,8 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa nativeConfig.enableAllSecurityServices |= Boolean.parseBoolean(prop.getValue()); } else if (prop.getKey().equals("quarkus.native.enable-all-charsets") && prop.getValue() != null) { nativeConfig.addAllCharsets |= Boolean.parseBoolean(prop.getValue()); + } else if (prop.getKey().equals("quarkus.native.enable-all-timezones") && prop.getValue() != null) { + enableAllTimeZones = Boolean.parseBoolean(prop.getValue()); } else { // todo maybe just -D is better than -J-D in this case if (prop.getValue() == null) { @@ -171,15 +199,12 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa nativeConfig.enableAllSecurityServices = true; } - if (nativeConfig.additionalBuildArgs != null) { - command.addAll(nativeConfig.additionalBuildArgs); - } + nativeConfig.additionalBuildArgs.ifPresent(l -> l.stream().map(String::trim).forEach(command::add)); command.add("--initialize-at-build-time="); command.add("-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime"); //the default collection policy results in full GC's 50% of the time command.add("-jar"); command.add(runnerJarName); - //https://github.com/oracle/graal/issues/660 - command.add("-J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1"); + if (nativeConfig.enableFallbackImages) { command.add("-H:FallbackThreshold=5"); } else { @@ -225,6 +250,9 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa } else { command.add("-H:-AddAllCharsets"); } + if (enableAllTimeZones) { + command.add("-H:+IncludeAllTimeZones"); + } if (!protocols.isEmpty()) { command.add("-H:EnableURLProtocols=" + String.join(",", protocols)); } @@ -238,7 +266,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa if (!nativeConfig.enableIsolates) { command.add("-H:-SpawnIsolates"); } - if (nativeConfig.enableJni) { + if (nativeConfig.enableJni || (graalVMVersion.isPresent() && !graalVMVersion.get().contains(" 19.2."))) { command.add("-H:+JNI"); } else { command.add("-H:-JNI"); @@ -292,23 +320,21 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa } } - //FIXME remove after transition period - private boolean isThisGraalVMVersionObsolete() { - final String vmName = System.getProperty("java.vm.name"); - log.info("Running Quarkus native-image plugin on " + vmName); - final List obsoleteGraalVmVersions = Arrays.asList("1.0.0", "19.0.", "19.1.", "19.2.0"); - final boolean vmVersionIsObsolete = obsoleteGraalVmVersions.stream().anyMatch(vmName::contains); + private void checkGraalVMVersion(String version) { + log.info("Running Quarkus native-image plugin on " + version); + final List obsoleteGraalVmVersions = Arrays.asList("1.0.0", "19.0.", "19.1.", "19.2.0", "19.3.0"); + final boolean vmVersionIsObsolete = obsoleteGraalVmVersions.stream().anyMatch(v -> version.contains(" " + v)); if (vmVersionIsObsolete) { - log.error("Out of date build of GraalVM detected! Please upgrade to GraalVM 19.2.1."); - return true; + throw new IllegalStateException("Unsupported version of GraalVM detected: " + version + "." + + " Quarkus currently offers a stable support of GraalVM 19.2.1 and a preview support of GraalVM 19.3.1." + + " Please upgrade GraalVM to one of these versions."); } - return false; } - private static File getNativeImageExecutable(String graalVmHome, File javaHome, Map env) { + private static File getNativeImageExecutable(Optional graalVmHome, File javaHome, Map env) { String imageName = IS_WINDOWS ? "native-image.cmd" : "native-image"; - if (graalVmHome != null) { - File file = Paths.get(graalVmHome, "bin", imageName).toFile(); + if (graalVmHome.isPresent()) { + File file = Paths.get(graalVmHome.get(), "bin", imageName).toFile(); if (file.exists()) { return file; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java index d8909704a6c0b..7d7a61d02d042 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java @@ -88,6 +88,8 @@ public interface AnnotationProxy { Map getDefaultValues(); + Map getValues(); + } public class AnnotationProxyBuilder { @@ -97,6 +99,7 @@ public class AnnotationProxyBuilder { private final AnnotationInstance annotationInstance; private final Class annotationType; private final Map defaultValues = new HashMap<>(); + private final Map values = new HashMap<>(); AnnotationProxyBuilder(AnnotationInstance annotationInstance, Class annotationType, String annotationLiteral, ClassInfo annotationClass) { @@ -106,6 +109,18 @@ public class AnnotationProxyBuilder { this.annotationClass = annotationClass; } + /** + * Explicit values override the default values from the annotation class. + * + * @param name + * @param value + * @return self + */ + public AnnotationProxyBuilder withValue(String name, Object value) { + values.put(name, value); + return this; + } + /** * Explicit default values override the default values from the annotation class. * @@ -180,6 +195,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return annotationInstance; case "getDefaultValues": return defaultValues; + case "getValues": + return values; default: break; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java index 29e47c8e8f713..c0f9977c0e031 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java @@ -14,7 +14,6 @@ import java.net.URL; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -238,6 +237,18 @@ public T getRecordingProxy(Class theClass) { T recordingProxy = factory.newInstance(new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (staticInit) { + for (int i = 0; i < args.length; ++i) { + if (args[i] instanceof ReturnedProxy) { + ReturnedProxy p = (ReturnedProxy) args[i]; + if (!p.__static$$init()) { + throw new RuntimeException("Invalid proxy passed to recorder. Parameter " + i + " of type " + + method.getParameterTypes()[i] + + " was created in a runtime recorder method, while this recorder is for a static init method. The object will not have been created at the time this method is run."); + } + } + } + } StoredMethodCall storedMethodCall = new StoredMethodCall(theClass, method, args); storedMethodCalls.add(storedMethodCall); Class returnType = method.getReturnType(); @@ -805,41 +816,54 @@ ResultHandle createValue(MethodContext context, MethodCreator method, ResultHand // new com.foo.MyAnnotation_Proxy_AnnotationLiteral("foo") AnnotationProxy annotationProxy = (AnnotationProxy) param; List constructorParams = annotationProxy.getAnnotationClass().methods().stream() - .filter(m -> !m.name().equals("") && !m.name().equals("")) - .collect(Collectors.toList()); + .filter(m -> !m.name().equals("") && !m.name().equals("")).collect(Collectors.toList()); Map annotationValues = annotationProxy.getAnnotationInstance().values().stream() .collect(Collectors.toMap(AnnotationValue::name, Function.identity())); DeferredParameter[] constructorParamsHandles = new DeferredParameter[constructorParams.size()]; for (ListIterator iterator = constructorParams.listIterator(); iterator.hasNext();) { MethodInfo valueMethod = iterator.next(); - AnnotationValue value = annotationValues.get(valueMethod.name()); - if (value == null) { - // method.invokeInterfaceMethod(MAP_PUT, valuesHandle, method.load(entry.getKey()), loadObjectInstance(method, entry.getValue(), returnValueResults, entry.getValue().getClass())); - Object defaultValue = annotationProxy.getDefaultValues().get(valueMethod.name()); - if (defaultValue != null) { - constructorParamsHandles[iterator.previousIndex()] = loadObjectInstance(defaultValue, - existing, defaultValue.getClass()); - continue; + Object explicitValue = annotationProxy.getValues().get(valueMethod.name()); + if (explicitValue != null) { + constructorParamsHandles[iterator.previousIndex()] = loadObjectInstance(explicitValue, existing, + explicitValue.getClass()); + } else { + AnnotationValue value = annotationValues.get(valueMethod.name()); + if (value == null) { + // method.invokeInterfaceMethod(MAP_PUT, valuesHandle, method.load(entry.getKey()), loadObjectInstance(method, entry.getValue(), + // returnValueResults, entry.getValue().getClass())); + Object defaultValue = annotationProxy.getDefaultValues().get(valueMethod.name()); + if (defaultValue != null) { + constructorParamsHandles[iterator.previousIndex()] = loadObjectInstance(defaultValue, existing, + defaultValue.getClass()); + continue; + } + if (value == null) { + value = valueMethod.defaultValue(); + } } if (value == null) { - value = valueMethod.defaultValue(); + throw new NullPointerException("Value not set for " + param); } + DeferredParameter retValue = loadValue(value, annotationProxy.getAnnotationClass(), valueMethod); + constructorParamsHandles[iterator.previousIndex()] = retValue; } - if (value == null) { - throw new NullPointerException("Value not set for " + param); - } - DeferredParameter retValue = loadValue(value, annotationProxy.getAnnotationClass(), valueMethod); - constructorParamsHandles[iterator.previousIndex()] = retValue; } return new DeferredArrayStoreParameter() { @Override ResultHandle createValue(MethodContext context, MethodCreator method, ResultHandle array) { - return method - .newInstance(MethodDescriptor.ofConstructor(annotationProxy.getAnnotationLiteralType(), - constructorParams.stream().map(m -> m.returnType().name().toString()).toArray()), - Arrays.stream(constructorParamsHandles).map(m -> context.loadDeferred(m)) - .toArray(ResultHandle[]::new)); + MethodDescriptor constructor = MethodDescriptor.ofConstructor(annotationProxy.getAnnotationLiteralType(), + constructorParams.stream().map(m -> m.returnType().name().toString()).toArray()); + ResultHandle[] args = new ResultHandle[constructorParamsHandles.length]; + for (int i = 0; i < constructorParamsHandles.length; i++) { + DeferredParameter deferredParameter = constructorParamsHandles[i]; + if (deferredParameter instanceof DeferredArrayStoreParameter) { + DeferredArrayStoreParameter arrayParam = (DeferredArrayStoreParameter) deferredParameter; + arrayParam.doPrepare(context); + } + args[i] = context.loadDeferred(deferredParameter); + } + return method.newInstance(constructor, args); } }; @@ -1043,7 +1067,7 @@ public void prepare(MethodContext context) { handledProperties.add(i.getName()); Collection propertyValue = (Collection) i.read(param); - if (!propertyValue.isEmpty()) { + if (propertyValue != null && !propertyValue.isEmpty()) { List params = new ArrayList<>(); for (Object c : propertyValue) { @@ -1082,7 +1106,7 @@ public void prepare(MethodContext context) { handledProperties.add(i.getName()); Map propertyValue = (Map) i.read(param); - if (!propertyValue.isEmpty()) { + if (propertyValue != null && !propertyValue.isEmpty()) { Map def = new LinkedHashMap<>(); for (Map.Entry entry : propertyValue.entrySet()) { DeferredParameter key = loadObjectInstance(entry.getKey(), existing, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ApplicationInfoBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ApplicationInfoBuildStep.java index 116a48df28e4d..04202643e0055 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ApplicationInfoBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ApplicationInfoBuildStep.java @@ -1,8 +1,8 @@ package io.quarkus.deployment.steps; -import io.quarkus.deployment.ApplicationConfig; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; +import io.quarkus.runtime.ApplicationConfig; public class ApplicationInfoBuildStep { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigBuildSteps.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigBuildSteps.java new file mode 100644 index 0000000000000..221f99b6eb645 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigBuildSteps.java @@ -0,0 +1,98 @@ +package io.quarkus.deployment.steps; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.eclipse.microprofile.config.spi.Converter; + +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.DeploymentClassLoaderBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigurationSourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.util.ServiceUtil; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.runtime.graal.InetRunTime; +import io.smallrye.config.SmallRyeConfigProviderResolver; + +class ConfigBuildSteps { + + static final String PROVIDER_CLASS_NAME = "io.quarkus.runtime.generated.ConfigSourceProviderImpl"; + + static final String SERVICES_PREFIX = "META-INF/services/"; + + @BuildStep + void generateConfigSources(List runTimeSources, + final BuildProducer generatedClass) { + ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClass, true); + + try (ClassCreator cc = ClassCreator.builder().interfaces(ConfigSourceProvider.class).setFinal(true) + .className(PROVIDER_CLASS_NAME) + .classOutput(classOutput).build()) { + try (MethodCreator mc = cc.getMethodCreator(MethodDescriptor.ofMethod(ConfigSourceProvider.class, + "getConfigSources", Iterable.class, ClassLoader.class))) { + + final ResultHandle array = mc.newArray(ConfigSource.class, mc.load(runTimeSources.size())); + for (int i = 0; i < runTimeSources.size(); i++) { + final RunTimeConfigurationSourceBuildItem runTimeSource = runTimeSources.get(i); + final String className = runTimeSource.getClassName(); + final OptionalInt priority = runTimeSource.getPriority(); + ResultHandle value; + if (priority.isPresent()) { + value = mc.newInstance(MethodDescriptor.ofConstructor(className, int.class), + mc.load(priority.getAsInt())); + } else { + value = mc.newInstance(MethodDescriptor.ofConstructor(className)); + } + mc.writeArrayValue(array, i, value); + } + final ResultHandle list = mc.invokeStaticMethod( + MethodDescriptor.ofMethod(Arrays.class, "asList", List.class, Object[].class), array); + mc.returnValue(list); + } + } + } + + // XXX replace this with constant-folded service loader impl + @BuildStep + void nativeServiceProviders( + final DeploymentClassLoaderBuildItem classLoaderItem, + final BuildProducer providerProducer) throws IOException { + providerProducer.produce(new ServiceProviderBuildItem(ConfigProviderResolver.class.getName(), + SmallRyeConfigProviderResolver.class.getName())); + final ClassLoader classLoader = classLoaderItem.getClassLoader(); + classLoader.getResources(SERVICES_PREFIX + ConfigSourceProvider.class.getName()); + for (Class serviceClass : Arrays.asList( + ConfigSource.class, + ConfigSourceProvider.class, + Converter.class)) { + final String serviceName = serviceClass.getName(); + final Set names = ServiceUtil.classNamesNamedIn(classLoader, SERVICES_PREFIX + serviceName); + final List list = names.stream() + // todo: see https://github.com/quarkusio/quarkus/issues/5492 + .filter(s -> !s.startsWith("org.jboss.resteasy.microprofile.config.")).collect(Collectors.toList()); + if (!list.isEmpty()) { + providerProducer.produce(new ServiceProviderBuildItem(serviceName, list)); + } + } + } + + @BuildStep + RuntimeInitializedClassBuildItem runtimeInitializedClass() { + return new RuntimeInitializedClassBuildItem(InetRunTime.class.getName()); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java index c2cb9e9da1a6c..c2f72b3a82a83 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java @@ -1,6 +1,7 @@ package io.quarkus.deployment.steps; import java.io.InputStream; +import java.lang.reflect.Field; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; @@ -8,21 +9,20 @@ import java.util.Properties; import java.util.function.Consumer; +import org.eclipse.microprofile.config.inject.ConfigProperty; + import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.BuildTimeConfigurationBuildItem; -import io.quarkus.deployment.builditem.BuildTimeRunTimeFixedConfigurationBuildItem; import io.quarkus.deployment.builditem.ConfigDescriptionBuildItem; -import io.quarkus.deployment.builditem.RunTimeConfigurationBuildItem; -import io.quarkus.deployment.configuration.ConfigDefinition; -import io.quarkus.deployment.configuration.LeafConfigType; +import io.quarkus.deployment.builditem.ConfigurationBuildItem; +import io.quarkus.deployment.configuration.matching.ConfigPatternMap; +import io.quarkus.deployment.configuration.matching.Container; +import io.quarkus.runtime.annotations.ConfigItem; public class ConfigDescriptionBuildStep { @BuildStep List createConfigDescriptions( - RunTimeConfigurationBuildItem runtimeConfig, - BuildTimeConfigurationBuildItem buildTimeConfig, - BuildTimeRunTimeFixedConfigurationBuildItem buildTimeRuntimeConfig) throws Exception { + ConfigurationBuildItem config) throws Exception { Properties javadoc = new Properties(); Enumeration resources = Thread.currentThread().getContextClassLoader() .getResources("META-INF/quarkus-javadoc.properties"); @@ -32,20 +32,46 @@ List createConfigDescriptions( } } List ret = new ArrayList<>(); - processConfig(runtimeConfig.getConfigDefinition(), ret, javadoc); - processConfig(buildTimeConfig.getConfigDefinition(), ret, javadoc); - processConfig(buildTimeRuntimeConfig.getConfigDefinition(), ret, javadoc); + processConfig(config.getReadResult().getBuildTimePatternMap(), ret, javadoc); + processConfig(config.getReadResult().getBuildTimeRunTimePatternMap(), ret, javadoc); + processConfig(config.getReadResult().getRunTimePatternMap(), ret, javadoc); return ret; } - private void processConfig(ConfigDefinition configDefinition, List ret, Properties javadoc) { + private void processConfig(ConfigPatternMap patterns, List ret, + Properties javadoc) { - configDefinition.getLeafPatterns().forEach(new Consumer() { + patterns.forEach(new Consumer() { @Override - public void accept(LeafConfigType leafConfigType) { - ret.add(new ConfigDescriptionBuildItem("quarkus." + leafConfigType.getConfigKey(), - leafConfigType.getItemClass(), - leafConfigType.getDefaultValueString(), javadoc.getProperty(leafConfigType.getJavadocKey()))); + public void accept(Container node) { + Field field = node.findField(); + ConfigItem configItem = field.getAnnotation(ConfigItem.class); + final ConfigProperty configProperty = field.getAnnotation(ConfigProperty.class); + String defaultDefault; + final Class valueClass = field.getType(); + if (valueClass == boolean.class) { + defaultDefault = "false"; + } else if (valueClass.isPrimitive() && valueClass != char.class) { + defaultDefault = "0"; + } else { + defaultDefault = null; + } + String defVal = defaultDefault; + if (configItem != null) { + final String itemDefVal = configItem.defaultValue(); + if (!itemDefVal.equals(ConfigItem.NO_DEFAULT)) { + defVal = itemDefVal; + } + } else if (configProperty != null) { + final String propDefVal = configProperty.defaultValue(); + if (!propDefVal.equals(ConfigProperty.UNCONFIGURED_VALUE)) { + defVal = propDefVal; + } + } + String javadocKey = field.getDeclaringClass().getName().replace("$", ".") + "." + field.getName(); + ret.add(new ConfigDescriptionBuildItem("quarkus." + node.getPropertyName(), + node.findEnclosingClass().getConfigurationClass(), + defVal, javadoc.getProperty(javadocKey))); } }); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigurationSetup.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigurationSetup.java deleted file mode 100644 index e72455a90b75a..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigurationSetup.java +++ /dev/null @@ -1,830 +0,0 @@ -package io.quarkus.deployment.steps; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.OptionalInt; -import java.util.Properties; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.UnaryOperator; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; -import org.eclipse.microprofile.config.spi.ConfigBuilder; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.ConfigSourceProvider; -import org.eclipse.microprofile.config.spi.Converter; -import org.graalvm.nativeimage.ImageInfo; -import org.jboss.logging.Logger; -import org.objectweb.asm.Opcodes; - -import io.quarkus.deployment.AccessorFinder; -import io.quarkus.deployment.ApplicationArchive; -import io.quarkus.deployment.GeneratedClassGizmoAdaptor; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; -import io.quarkus.deployment.builditem.ArchiveRootBuildItem; -import io.quarkus.deployment.builditem.BuildTimeRunTimeFixedConfigurationBuildItem; -import io.quarkus.deployment.builditem.BytecodeRecorderObjectLoaderBuildItem; -import io.quarkus.deployment.builditem.ConfigurationTypeBuildItem; -import io.quarkus.deployment.builditem.ExtensionClassLoaderBuildItem; -import io.quarkus.deployment.builditem.GeneratedClassBuildItem; -import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; -import io.quarkus.deployment.builditem.RunTimeConfigurationBuildItem; -import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; -import io.quarkus.deployment.builditem.RunTimeConfigurationSourceBuildItem; -import io.quarkus.deployment.builditem.UnmatchedConfigBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; -import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; -import io.quarkus.deployment.configuration.ConfigDefinition; -import io.quarkus.deployment.configuration.ConfigPatternMap; -import io.quarkus.deployment.configuration.LeafConfigType; -import io.quarkus.deployment.recording.ObjectLoader; -import io.quarkus.deployment.util.ServiceUtil; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.ClassOutput; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.runtime.annotations.ConfigRoot; -import io.quarkus.runtime.configuration.AbstractRawDefaultConfigSource; -import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSource; -import io.quarkus.runtime.configuration.BuildTimeConfigFactory; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.ConverterSupport; -import io.quarkus.runtime.configuration.DefaultConfigSource; -import io.quarkus.runtime.configuration.DeploymentProfileConfigSource; -import io.quarkus.runtime.configuration.ExpandingConfigSource; -import io.quarkus.runtime.configuration.HyphenateEnumConverter; -import io.quarkus.runtime.configuration.NameIterator; -import io.quarkus.runtime.configuration.ProfileManager; -import io.quarkus.runtime.configuration.SimpleConfigurationProviderResolver; -import io.quarkus.runtime.graal.InetRunTime; -import io.smallrye.config.Converters; -import io.smallrye.config.SmallRyeConfig; -import io.smallrye.config.SmallRyeConfigBuilder; - -/** - * Setup steps for configuration purposes. - */ -public class ConfigurationSetup { - - private static final Logger log = Logger.getLogger("io.quarkus.configuration"); - - public static final String BUILD_TIME_CONFIG = "io.quarkus.runtime.generated.BuildTimeConfig"; - public static final String BUILD_TIME_CONFIG_ROOT = "io.quarkus.runtime.generated.BuildTimeConfigRoot"; - public static final String RUN_TIME_CONFIG = "io.quarkus.runtime.generated.RunTimeConfig"; - public static final String RUN_TIME_CONFIG_ROOT = "io.quarkus.runtime.generated.RunTimeConfigRoot"; - public static final String RUN_TIME_DEFAULTS = "io.quarkus.runtime.generated.RunTimeDefaultConfigSource"; - - public static final MethodDescriptor CREATE_RUN_TIME_CONFIG = MethodDescriptor.ofMethod(RUN_TIME_CONFIG, - "getRunTimeConfiguration", void.class); - public static final MethodDescriptor ECS_EXPAND_VALUE = MethodDescriptor.ofMethod(ExpandingConfigSource.class, - "expandValue", - String.class, String.class, ExpandingConfigSource.Cache.class); - - private static final FieldDescriptor RUN_TIME_CONFIG_FIELD = FieldDescriptor.of(RUN_TIME_CONFIG, "runConfig", - RUN_TIME_CONFIG_ROOT); - private static final FieldDescriptor BUILD_TIME_CONFIG_FIELD = FieldDescriptor.of(BUILD_TIME_CONFIG, "buildConfig", - BUILD_TIME_CONFIG_ROOT); - private static final FieldDescriptor CONVERTERS_FIELD = FieldDescriptor.of(BUILD_TIME_CONFIG, "converters", - Converter[].class); - - private static final MethodDescriptor NI_HAS_NEXT = MethodDescriptor.ofMethod(NameIterator.class, "hasNext", boolean.class); - private static final MethodDescriptor NI_NEXT_EQUALS = MethodDescriptor.ofMethod(NameIterator.class, "nextSegmentEquals", - boolean.class, String.class); - private static final MethodDescriptor NI_NEXT = MethodDescriptor.ofMethod(NameIterator.class, "next", void.class); - private static final MethodDescriptor ITR_HAS_NEXT = MethodDescriptor.ofMethod(Iterator.class, "hasNext", boolean.class); - private static final MethodDescriptor ITR_NEXT = MethodDescriptor.ofMethod(Iterator.class, "next", Object.class); - private static final MethodDescriptor C_GET_IMPLICIT_CONVERTER = MethodDescriptor.ofMethod(Converters.class, - "getImplicitConverter", Converter.class, Class.class); - private static final MethodDescriptor CPR_SET_INSTANCE = MethodDescriptor.ofMethod(ConfigProviderResolver.class, - "setInstance", void.class, ConfigProviderResolver.class); - private static final MethodDescriptor CPR_REGISTER_CONFIG = MethodDescriptor.ofMethod(ConfigProviderResolver.class, - "registerConfig", void.class, Config.class, ClassLoader.class); - private static final MethodDescriptor CPR_INSTANCE = MethodDescriptor.ofMethod(ConfigProviderResolver.class, - "instance", ConfigProviderResolver.class); - private static final MethodDescriptor SCPR_CONSTRUCT = MethodDescriptor - .ofConstructor(SimpleConfigurationProviderResolver.class); - private static final MethodDescriptor SRCB_BUILD = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, "build", - Config.class); - private static final MethodDescriptor SRCB_WITH_CONVERTER = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, - "withConverter", ConfigBuilder.class, Class.class, int.class, Converter.class); - private static final MethodDescriptor SRCB_WITH_SOURCES = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, - "withSources", ConfigBuilder.class, ConfigSource[].class); - private static final MethodDescriptor SRCB_ADD_DEFAULT_SOURCES = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, - "addDefaultSources", ConfigBuilder.class); - private static final MethodDescriptor SRCB_ADD_DISCOVERED_SOURCES = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, - "addDiscoveredSources", ConfigBuilder.class); - private static final MethodDescriptor SRCB_CONSTRUCT = MethodDescriptor.ofConstructor(SmallRyeConfigBuilder.class); - private static final MethodDescriptor II_IN_IMAGE_RUN = MethodDescriptor.ofMethod(ImageInfo.class, "inImageRuntimeCode", - boolean.class); - private static final MethodDescriptor SRCB_WITH_WRAPPER = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, - "withWrapper", SmallRyeConfigBuilder.class, UnaryOperator.class); - - private static final MethodDescriptor BTCF_GET_CONFIG_SOURCE = MethodDescriptor.ofMethod(BuildTimeConfigFactory.class, - "getBuildTimeConfigSource", ConfigSource.class); - private static final MethodDescriptor ECS_CACHE_CONSTRUCT = MethodDescriptor - .ofConstructor(ExpandingConfigSource.Cache.class); - private static final MethodDescriptor ECS_WRAPPER = MethodDescriptor.ofMethod(ExpandingConfigSource.class, "wrapper", - UnaryOperator.class, ExpandingConfigSource.Cache.class); - - private static final MethodDescriptor PROFILE_WRAPPER = MethodDescriptor.ofMethod(DeploymentProfileConfigSource.class, - "wrapper", - UnaryOperator.class); - - private static final MethodDescriptor RTD_CTOR = MethodDescriptor.ofConstructor(RUN_TIME_DEFAULTS); - private static final MethodDescriptor RTD_GET_VALUE = MethodDescriptor.ofMethod(RUN_TIME_DEFAULTS, "getValue", String.class, - NameIterator.class); - private static final MethodDescriptor ARDCS_CTOR = MethodDescriptor.ofConstructor(AbstractRawDefaultConfigSource.class); - - private static final MethodDescriptor CS_POPULATE_CONVERTERS = MethodDescriptor.ofMethod(ConverterSupport.class, - "populateConverters", void.class, ConfigBuilder.class); - - private static final MethodDescriptor SET_RUNTIME_DEFAULT_PROFILE = MethodDescriptor.ofMethod(ProfileManager.class, - "setRuntimeDefaultProfile", void.class, String.class); - private static final MethodDescriptor HYPHENATED_ENUM_CONVERTER_CTOR = MethodDescriptor - .ofConstructor(HyphenateEnumConverter.class, Class.class); - private static final MethodDescriptor CU_EXPLICIT_RUNTIME_CONVERTER = MethodDescriptor.ofMethod(ConfigUtils.class, - "populateExplicitRuntimeConverter", void.class, Class.class, Class.class, Converter.class); - - private static final String[] NO_STRINGS = new String[0]; - - public ConfigurationSetup() { - } - - /** - * Run before anything that consumes configuration; sets up the main configuration definition instance. - * - * @param runTimeConfigItem the run time config item - * @param buildTimeRunTimeConfigItem the build time/run time fixed config item - * @param resourceConsumer - * @param niResourceConsumer - * @param runTimeDefaultConsumer - * @param unmatchedConfigBuildItem the build item holding the unmatched config keys - * @param extensionClassLoaderBuildItem the extension class loader build item - * @param archiveRootBuildItem the application archive root - * @throws IOException - */ - @BuildStep - public void initializeConfiguration( - RunTimeConfigurationBuildItem runTimeConfigItem, - BuildTimeRunTimeFixedConfigurationBuildItem buildTimeRunTimeConfigItem, - Consumer resourceConsumer, - Consumer niResourceConsumer, - Consumer runTimeDefaultConsumer, - UnmatchedConfigBuildItem unmatchedConfigBuildItem, - ExtensionClassLoaderBuildItem extensionClassLoaderBuildItem, - ArchiveRootBuildItem archiveRootBuildItem) throws IOException { - - SmallRyeConfig src = (SmallRyeConfig) ConfigProvider.getConfig(); - - final ConfigDefinition runTimeConfig = runTimeConfigItem.getConfigDefinition(); - final ConfigDefinition buildTimeRunTimeConfig = buildTimeRunTimeConfigItem.getConfigDefinition(); - - // store the expanded values from the build - final byte[] bytes; - try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { - try (OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { - final Properties properties = new Properties(); - properties.putAll(buildTimeRunTimeConfig.getLoadedProperties()); - properties.store(osw, "This file is generated from captured build-time values; do not edit this file manually"); - } - os.flush(); - bytes = os.toByteArray(); - } - resourceConsumer.accept( - new GeneratedResourceBuildItem(BuildTimeConfigFactory.BUILD_TIME_CONFIG_NAME, bytes)); - niResourceConsumer.accept( - new NativeImageResourceBuildItem(BuildTimeConfigFactory.BUILD_TIME_CONFIG_NAME)); - - // produce defaults for user-provided config - - final Set unmatched = new HashSet<>(); - unmatched.addAll(unmatchedConfigBuildItem.getSet()); - unmatched.addAll(runTimeConfig.getLoadedProperties().keySet()); - final boolean old = ExpandingConfigSource.setExpanding(false); - try { - for (String propName : unmatched) { - runTimeDefaultConsumer - .accept(new RunTimeConfigurationDefaultBuildItem(propName, - src.getOptionalValue(propName, String.class).orElse(""))); - } - } finally { - ExpandingConfigSource.setExpanding(old); - } - - } - - @BuildStep - public void addDiscoveredSources(ApplicationArchivesBuildItem archives, Consumer providerConsumer) - throws IOException { - final Collection sources = new LinkedHashSet<>(); - final Collection sourceProviders = new LinkedHashSet<>(); - for (ApplicationArchive archive : archives.getAllApplicationArchives()) { - Path childPath = archive.getChildPath("META-INF/services/" + ConfigSource.class.getName()); - if (childPath != null) { - sources.addAll(ServiceUtil.classNamesNamedIn(childPath)); - } - childPath = archive.getChildPath("META-INF/services/" + ConfigSourceProvider.class.getName()); - if (childPath != null) { - sourceProviders.addAll(ServiceUtil.classNamesNamedIn(childPath)); - } - } - if (sources.size() > 0) { - providerConsumer.accept(new ServiceProviderBuildItem(ConfigSource.class.getName(), sources.toArray(NO_STRINGS))); - } - if (sourceProviders.size() > 0) { - providerConsumer.accept( - new ServiceProviderBuildItem(ConfigSourceProvider.class.getName(), sourceProviders.toArray(NO_STRINGS))); - } - } - - /** - * Add a config sources for {@code application.properties}. - */ - @BuildStep - void setUpConfigFile(BuildProducer configSourceConsumer) { - configSourceConsumer.produce(new RunTimeConfigurationSourceBuildItem( - ApplicationPropertiesConfigSource.InJar.class.getName(), OptionalInt.empty())); - configSourceConsumer.produce(new RunTimeConfigurationSourceBuildItem( - ApplicationPropertiesConfigSource.InFileSystem.class.getName(), OptionalInt.empty())); - } - - /** - * Write the default run time configuration. - */ - @BuildStep - RunTimeConfigurationSourceBuildItem writeDefaults( - List defaults, - Consumer resourceConsumer, - Consumer niResourceConsumer) throws IOException { - final Properties properties = new Properties(); - for (RunTimeConfigurationDefaultBuildItem item : defaults) { - final String key = item.getKey(); - final String value = item.getValue(); - final String existing = properties.getProperty(key); - if (existing != null && !existing.equals(value)) { - log.warnf( - "Two conflicting default values were specified for configuration key \"%s\": \"%s\" and \"%s\" (using \"%2$s\")", - key, - existing, - value); - } else { - properties.setProperty(key, value); - } - } - try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { - try (OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { - properties.store(osw, "This is the generated set of default configuration values"); - osw.flush(); - resourceConsumer.accept( - new GeneratedResourceBuildItem(DefaultConfigSource.DEFAULT_CONFIG_PROPERTIES_NAME, os.toByteArray())); - niResourceConsumer.accept( - new NativeImageResourceBuildItem(DefaultConfigSource.DEFAULT_CONFIG_PROPERTIES_NAME)); - } - } - return new RunTimeConfigurationSourceBuildItem(DefaultConfigSource.class.getName(), OptionalInt.empty()); - } - - /** - * Generate the bytecode to load configuration objects at static init and run time. - * - * @param runTimeConfigItem the config build item - * @param classConsumer the consumer of generated classes - * @param runTimeInitConsumer the consumer of runtime init classes - */ - @BuildStep - void finalizeConfigLoader( - RunTimeConfigurationBuildItem runTimeConfigItem, - BuildTimeRunTimeFixedConfigurationBuildItem buildTimeRunTimeConfigItem, - BuildProducer classConsumer, - Consumer runTimeInitConsumer, - Consumer objectLoaderConsumer, - List configTypeItems, - List runTimeSources) { - final ClassOutput classOutput = new GeneratedClassGizmoAdaptor(classConsumer, true); - - // General run time setup - - AccessorFinder accessorFinder = new AccessorFinder(); - - final ConfigDefinition runTimeConfigDef = runTimeConfigItem.getConfigDefinition(); - final ConfigPatternMap runTimePatterns = runTimeConfigDef.getLeafPatterns(); - - runTimeConfigDef.generateConfigRootClass(classOutput, accessorFinder); - - final ConfigDefinition buildTimeConfigDef = buildTimeRunTimeConfigItem.getConfigDefinition(); - final ConfigPatternMap buildTimePatterns = buildTimeConfigDef.getLeafPatterns(); - - buildTimeConfigDef.generateConfigRootClass(classOutput, accessorFinder); - - // Traverse all known run-time config types and ensure we have converters for them when image building runs - // This code is specific to native image and run time config, because the build time config is read during static init - - final HashSet> encountered = new HashSet<>(); - final ArrayList> configTypes = new ArrayList<>(); - for (ConfigurationTypeBuildItem item : configTypeItems) { - configTypes.add(item.getValueType()); - } - - for (LeafConfigType item : runTimePatterns) { - final Class typeClass = item.getItemClass(); - if (!typeClass.isPrimitive() && encountered.add(typeClass) - && Converters.getImplicitConverter(typeClass) != null) { - configTypes.add(typeClass); - } - } - - // stability - configTypes.sort(Comparator.comparing(Class::getName)); - int converterCnt = configTypes.size(); - - // Build time configuration class, also holds converters - try (final ClassCreator cc = new ClassCreator(classOutput, BUILD_TIME_CONFIG, null, Object.class.getName())) { - // field to stash converters into - cc.getFieldCreator(CONVERTERS_FIELD).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); - // holder for the build-time configuration - cc.getFieldCreator(BUILD_TIME_CONFIG_FIELD) - .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_VOLATILE); - - // static init block - try (MethodCreator clinit = cc.getMethodCreator("", void.class)) { - clinit.setModifiers(Opcodes.ACC_STATIC); - // set default profile to build profile - clinit.invokeStaticMethod(SET_RUNTIME_DEFAULT_PROFILE, clinit.load(ProfileManager.getActiveProfile())); - - // make implicit converters available to native image run time - final BranchResult inImageBuild = clinit.ifNonZero(clinit - .invokeStaticMethod(MethodDescriptor.ofMethod(ImageInfo.class, "inImageBuildtimeCode", boolean.class))); - try (BytecodeCreator yes = inImageBuild.trueBranch()) { - - final ResultHandle array = yes.newArray(Converter.class, yes.load(converterCnt)); - for (int i = 0; i < converterCnt; i++) { - yes.writeArrayValue(array, i, - yes.invokeStaticMethod(C_GET_IMPLICIT_CONVERTER, yes.loadClass(configTypes.get(i)))); - } - yes.writeStaticField(CONVERTERS_FIELD, array); - } - try (BytecodeCreator no = inImageBuild.falseBranch()) { - no.writeStaticField(CONVERTERS_FIELD, no.loadNull()); - } - - // create build time configuration object - - final ResultHandle builder = clinit.newInstance(SRCB_CONSTRUCT); - // todo: custom build time converters - final ResultHandle array = clinit.newArray(ConfigSource[].class, clinit.load(1)); - clinit.writeArrayValue(array, 0, clinit.invokeStaticMethod(BTCF_GET_CONFIG_SOURCE)); - clinit.invokeVirtualMethod(SRCB_WITH_SOURCES, builder, array); - // add default sources, which are only visible during static init - clinit.invokeVirtualMethod(SRCB_ADD_DEFAULT_SOURCES, builder); - - // create the actual config object - final ResultHandle config = clinit.checkCast(clinit.invokeVirtualMethod(SRCB_BUILD, builder), - SmallRyeConfig.class); - - // create the config root - clinit.writeStaticField(BUILD_TIME_CONFIG_FIELD, clinit - .newInstance(MethodDescriptor.ofConstructor(BUILD_TIME_CONFIG_ROOT, SmallRyeConfig.class), config)); - - // write out the parsing for the stored build time config - writeParsing(cc, clinit, config, null, buildTimePatterns); - - clinit.returnValue(null); - } - } - - // Run time configuration class - try (final ClassCreator cc = new ClassCreator(classOutput, RUN_TIME_CONFIG, null, Object.class.getName())) { - // holder for the run-time configuration - cc.getFieldCreator(RUN_TIME_CONFIG_FIELD) - .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_VOLATILE); - - // config object initialization - try (MethodCreator carc = cc.getMethodCreator(ConfigurationSetup.CREATE_RUN_TIME_CONFIG)) { - carc.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); - - // create run time configuration object - final ResultHandle builder = carc.newInstance(SRCB_CONSTRUCT); - carc.invokeVirtualMethod(SRCB_ADD_DEFAULT_SOURCES, builder); - - // discovered sources - carc.invokeVirtualMethod(SRCB_ADD_DISCOVERED_SOURCES, builder); - - // custom run time sources - final int size = runTimeSources.size(); - if (size > 0) { - final ResultHandle arrayHandle = carc.newArray(ConfigSource[].class, carc.load(size)); - for (int i = 0; i < size; i++) { - final RunTimeConfigurationSourceBuildItem source = runTimeSources.get(i); - final OptionalInt priority = source.getPriority(); - final ResultHandle val; - if (priority.isPresent()) { - val = carc.newInstance(MethodDescriptor.ofConstructor(source.getClassName(), int.class), - carc.load(priority.getAsInt())); - } else { - val = carc.newInstance(MethodDescriptor.ofConstructor(source.getClassName())); - } - carc.writeArrayValue(arrayHandle, i, val); - } - carc.invokeVirtualMethod( - SRCB_WITH_SOURCES, - builder, - arrayHandle); - } - // default value source - final ResultHandle defaultSourceArray = carc.newArray(ConfigSource[].class, carc.load(1)); - carc.writeArrayValue(defaultSourceArray, 0, carc.newInstance(RTD_CTOR)); - carc.invokeVirtualMethod(SRCB_WITH_SOURCES, builder, defaultSourceArray); - - // custom run time converters - carc.invokeStaticMethod(CS_POPULATE_CONVERTERS, builder); - - // cache explicit converts and make them available during runtime - for (LeafConfigType item : runTimePatterns) { - final Class typeClass = item.getItemClass(); - Class> itemConverterClass = item.getConverterClass(); - if (itemConverterClass == null) { - continue; - } - - ResultHandle typeClassHandle = carc.loadClass(typeClass); - final ResultHandle converter; - if (HyphenateEnumConverter.class.equals(itemConverterClass)) { - converter = carc.newInstance(HYPHENATED_ENUM_CONVERTER_CTOR, typeClassHandle); - } else { - converter = carc.newInstance(MethodDescriptor.ofConstructor(itemConverterClass)); - } - - carc.invokeStaticMethod(CU_EXPLICIT_RUNTIME_CONVERTER, typeClassHandle, carc.loadClass(itemConverterClass), - converter); - } - - // property expansion - final ResultHandle cache = carc.newInstance(ECS_CACHE_CONSTRUCT); - ResultHandle wrapper = carc.invokeStaticMethod(ECS_WRAPPER, cache); - carc.invokeVirtualMethod(SRCB_WITH_WRAPPER, builder, wrapper); - - //profiles - wrapper = carc.invokeStaticMethod(PROFILE_WRAPPER); - carc.invokeVirtualMethod(SRCB_WITH_WRAPPER, builder, wrapper); - - // write out loader for converter types - final BranchResult imgRun = carc.ifNonZero(carc.invokeStaticMethod(II_IN_IMAGE_RUN)); - try (BytecodeCreator inImageRun = imgRun.trueBranch()) { - final ResultHandle array = inImageRun.readStaticField(CONVERTERS_FIELD); - for (int i = 0; i < converterCnt; i++) { - // implicit converters will have a priority of 100. - inImageRun.invokeVirtualMethod( - SRCB_WITH_CONVERTER, - builder, - inImageRun.loadClass(configTypes.get(i)), - inImageRun.load(100), - inImageRun.readArrayValue(array, i)); - } - } - - // Build the config - - final ResultHandle config = carc.checkCast(carc.invokeVirtualMethod(SRCB_BUILD, builder), SmallRyeConfig.class); - - // IMPL NOTE: we do invoke ConfigProviderResolver.setInstance() in RUNTIME_INIT when an app starts, but ConfigProvider only obtains the - // resolver once when initializing ConfigProvider.INSTANCE. That is why we store the current Config as a static field on the - // SimpleConfigurationProviderResolver - carc.invokeStaticMethod(CPR_SET_INSTANCE, carc.newInstance(SCPR_CONSTRUCT)); - carc.invokeVirtualMethod(CPR_REGISTER_CONFIG, carc.invokeStaticMethod(CPR_INSTANCE), config, carc.loadNull()); - - // create the config root - carc.writeStaticField(RUN_TIME_CONFIG_FIELD, - carc.newInstance(MethodDescriptor.ofConstructor(RUN_TIME_CONFIG_ROOT, SmallRyeConfig.class), config)); - - writeParsing(cc, carc, config, cache, runTimePatterns); - - carc.returnValue(null); - } - } - - // now construct the default values class - try (ClassCreator cc = ClassCreator - .builder() - .classOutput(classOutput) - .className(RUN_TIME_DEFAULTS) - .superClass(AbstractRawDefaultConfigSource.class) - .build()) { - - // constructor - try (MethodCreator ctor = cc.getMethodCreator(RTD_CTOR)) { - ctor.setModifiers(Opcodes.ACC_PUBLIC); - ctor.invokeSpecialMethod(ARDCS_CTOR, ctor.getThis()); - ctor.returnValue(null); - } - - try (MethodCreator gv = cc.getMethodCreator(RTD_GET_VALUE)) { - final ResultHandle nameIter = gv.getMethodParam(0); - // if (! nameIter.hasNext()) return null; - gv.ifNonZero(gv.invokeVirtualMethod(NI_HAS_NEXT, nameIter)).falseBranch().returnValue(gv.loadNull()); - // if (! nameIter.nextSegmentEquals("quarkus")) return null; - gv.ifNonZero(gv.invokeVirtualMethod(NI_NEXT_EQUALS, nameIter, gv.load("quarkus"))).falseBranch() - .returnValue(gv.loadNull()); - // nameIter.next(); // skip "quarkus" - gv.invokeVirtualMethod(NI_NEXT, nameIter); - // return getValue_xx(nameIter); - gv.returnValue(gv.invokeVirtualMethod( - generateGetValue(cc, runTimePatterns, new StringBuilder("getValue"), new HashMap<>()), gv.getThis(), - nameIter)); - } - } - - objectLoaderConsumer.accept(new BytecodeRecorderObjectLoaderBuildItem(new ObjectLoader() { - public ResultHandle load(final BytecodeCreator body, final Object obj, final boolean staticInit) { - if (!canHandleObject(obj, staticInit)) { - return null; - } - boolean buildTime = false; - ConfigDefinition.RootInfo rootInfo = runTimeConfigDef.getInstanceInfo(obj); - if (rootInfo == null) { - rootInfo = buildTimeConfigDef.getInstanceInfo(obj); - buildTime = true; - } - final FieldDescriptor fieldDescriptor = rootInfo.getFieldDescriptor(); - final ResultHandle configRoot = body - .readStaticField(buildTime ? BUILD_TIME_CONFIG_FIELD : RUN_TIME_CONFIG_FIELD); - return body.readInstanceField(fieldDescriptor, configRoot); - } - - @Override - public boolean canHandleObject(Object obj, boolean staticInit) { - boolean buildTime = false; - ConfigDefinition.RootInfo rootInfo = runTimeConfigDef.getInstanceInfo(obj); - if (rootInfo == null) { - rootInfo = buildTimeConfigDef.getInstanceInfo(obj); - buildTime = true; - } - if (rootInfo == null || staticInit && !buildTime) { - final Class objClass = obj.getClass(); - if (objClass.isAnnotationPresent(ConfigRoot.class)) { - String msg = String.format( - "You are trying to use a ConfigRoot[%s] at static initialization time", - objClass.getName()); - throw new IllegalStateException(msg); - } - return false; - } - return true; - } - })); - - runTimeInitConsumer.accept(new RuntimeInitializedClassBuildItem(RUN_TIME_CONFIG)); - } - - private MethodDescriptor generateGetValue(final ClassCreator cc, final ConfigPatternMap keyMap, - final StringBuilder methodName, final Map cache) { - final String methodNameStr = methodName.toString(); - final MethodDescriptor existing = cache.get(methodNameStr); - if (existing != null) { - return existing; - } - try (MethodCreator body = cc.getMethodCreator(methodNameStr, String.class, NameIterator.class)) { - body.setModifiers(Opcodes.ACC_PROTECTED); - final ResultHandle nameIter = body.getMethodParam(0); - final LeafConfigType matched = keyMap.getMatched(); - // if (! keyIter.hasNext()) { - try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, nameIter)).falseBranch()) { - if (matched != null) { - // (exact match generated code) - matchedBody.returnValue( - matchedBody.load(matched.getDefaultValueString())); - } else { - // return; - matchedBody.returnValue(matchedBody.loadNull()); - } - } - // } - // branches for each next-string - boolean hasWildCard = false; - final Iterable names = keyMap.childNames(); - for (String name : names) { - if (name.equals(ConfigPatternMap.WILD_CARD)) { - hasWildCard = true; - } else { - // TODO: string switch - // if (keyIter.nextSegmentEquals(name)) { - try (BytecodeCreator nameMatched = body - .ifNonZero(body.invokeVirtualMethod(NI_NEXT_EQUALS, nameIter, body.load(name))).trueBranch()) { - // keyIter.next(); - nameMatched.invokeVirtualMethod(NI_NEXT, nameIter); - // (generated recursive) - final int length = methodName.length(); - methodName.append('_').append(name); - // result = this.getValue_xxx(nameIter); - final ResultHandle result = nameMatched.invokeVirtualMethod( - generateGetValue(cc, keyMap.getChild(name), methodName, cache), nameMatched.getThis(), - nameIter); - methodName.setLength(length); - // return result; - nameMatched.returnValue(result); - } - // } - } - } - if (hasWildCard) { - // consume and parse - try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, nameIter)) - .trueBranch()) { - // keyIter.next(); - matchedBody.invokeVirtualMethod(NI_NEXT, nameIter); - // (generated recursive) - final int length = methodName.length(); - methodName.append('_').append("wildcard"); - // result = this.getValue_xxx(nameIter); - final ResultHandle result = matchedBody.invokeVirtualMethod( - generateGetValue(cc, keyMap.getChild(ConfigPatternMap.WILD_CARD), methodName, cache), - matchedBody.getThis(), nameIter); - methodName.setLength(length); - // return result; - matchedBody.returnValue(result); - } - } - // it's not found - body.returnValue(body.loadNull()); - final MethodDescriptor md = body.getMethodDescriptor(); - cache.put(methodNameStr, md); - return md; - } - } - - private void writeParsing(final ClassCreator cc, final BytecodeCreator body, final ResultHandle config, - final ResultHandle cache, final ConfigPatternMap keyMap) { - // setup - // Iterable iterable = config.getPropertyNames(); - final ResultHandle iterable = body.invokeVirtualMethod( - MethodDescriptor.ofMethod(SmallRyeConfig.class, "getPropertyNames", Iterable.class), config); - // Iterator iterator = iterable.iterator(); - final ResultHandle iterator = body - .invokeInterfaceMethod(MethodDescriptor.ofMethod(Iterable.class, "iterator", Iterator.class), iterable); - - // loop: { - try (BytecodeCreator loop = body.createScope()) { - // if (iterator.hasNext()) - final BranchResult ifHasNext = loop.ifNonZero(loop.invokeInterfaceMethod(ITR_HAS_NEXT, iterator)); - // { - try (BytecodeCreator hasNext = ifHasNext.trueBranch()) { - // key = iterator.next(); - final ResultHandle key = hasNext.checkCast(hasNext.invokeInterfaceMethod(ITR_NEXT, iterator), String.class); - // NameIterator keyIter = new NameIterator(key); - final ResultHandle keyIter = hasNext - .newInstance(MethodDescriptor.ofConstructor(NameIterator.class, String.class), key); - // if (! keyIter.hasNext()) continue loop; - hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_HAS_NEXT, keyIter)).falseBranch().continueScope(loop); - // if (! keyIter.nextSegmentEquals("quarkus")) continue loop; - hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, hasNext.load("quarkus"))).falseBranch() - .continueScope(loop); - // keyIter.next(); // skip "quarkus" - hasNext.invokeVirtualMethod(NI_NEXT, keyIter); - // parse(config, cache, keyIter); - or - parse(config, keyIter); - final ResultHandle[] args; - final boolean expand = cache != null; - if (expand) { - args = new ResultHandle[] { config, cache, keyIter }; - } else { - args = new ResultHandle[] { config, keyIter }; - } - hasNext.invokeStaticMethod( - generateParserBody(cc, keyMap, new StringBuilder("parseKey"), new HashMap<>(), expand), - args); - // continue loop; - hasNext.continueScope(loop); - } - // } - } - // } - body.returnValue(body.loadNull()); - } - - private MethodDescriptor generateParserBody(final ClassCreator cc, final ConfigPatternMap keyMap, - final StringBuilder methodName, final Map parseMethodCache, final boolean expand) { - final String methodNameStr = methodName.toString(); - final MethodDescriptor existing = parseMethodCache.get(methodNameStr); - if (existing != null) { - return existing; - } - final Class[] argTypes; - if (expand) { - argTypes = new Class[] { SmallRyeConfig.class, ExpandingConfigSource.Cache.class, NameIterator.class }; - } else { - argTypes = new Class[] { SmallRyeConfig.class, NameIterator.class }; - } - try (MethodCreator body = cc.getMethodCreator(methodName.toString(), void.class, - argTypes)) { - body.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); - final ResultHandle config = body.getMethodParam(0); - final ResultHandle cache = expand ? body.getMethodParam(1) : null; - final ResultHandle keyIter = expand ? body.getMethodParam(2) : body.getMethodParam(1); - final LeafConfigType matched = keyMap.getMatched(); - // if (! keyIter.hasNext()) { - try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)).falseBranch()) { - if (matched != null) { - // (exact match generated code) - matched.generateAcceptConfigurationValue(matchedBody, keyIter, cache, config); - } else { - // todo: unknown name warning goes here - } - // return; - matchedBody.returnValue(null); - } - // } - // branches for each next-string - boolean hasWildCard = false; - final Iterable names = keyMap.childNames(); - for (String name : names) { - if (name.equals(ConfigPatternMap.WILD_CARD)) { - hasWildCard = true; - } else { - // TODO: string switch - // if (keyIter.nextSegmentEquals(name)) { - try (BytecodeCreator nameMatched = body - .ifNonZero(body.invokeVirtualMethod(NI_NEXT_EQUALS, keyIter, body.load(name))).trueBranch()) { - // keyIter.next(); - nameMatched.invokeVirtualMethod(NI_NEXT, keyIter); - // (generated recursive) - final int length = methodName.length(); - methodName.append('_').append(name); - final ResultHandle[] args; - if (expand) { - args = new ResultHandle[] { config, cache, keyIter }; - } else { - args = new ResultHandle[] { config, keyIter }; - } - nameMatched.invokeStaticMethod( - generateParserBody(cc, keyMap.getChild(name), methodName, parseMethodCache, expand), - args); - methodName.setLength(length); - // return; - nameMatched.returnValue(null); - } - // } - } - } - if (hasWildCard) { - // consume and parse - try (BytecodeCreator matchedBody = body.ifNonZero(body.invokeVirtualMethod(NI_HAS_NEXT, keyIter)) - .trueBranch()) { - // keyIter.next(); - matchedBody.invokeVirtualMethod(NI_NEXT, keyIter); - // (generated recursive) - final int length = methodName.length(); - methodName.append('_').append("wildcard"); - final ResultHandle[] args; - if (expand) { - args = new ResultHandle[] { config, cache, keyIter }; - } else { - args = new ResultHandle[] { config, keyIter }; - } - matchedBody.invokeStaticMethod( - generateParserBody(cc, keyMap.getChild(ConfigPatternMap.WILD_CARD), methodName, parseMethodCache, - expand), - args); - methodName.setLength(length); - // return; - matchedBody.returnValue(null); - } - } - // todo: unknown name warning goes here - body.returnValue(null); - final MethodDescriptor md = body.getMethodDescriptor(); - parseMethodCache.put(methodNameStr, md); - return md; - } - } - - @BuildStep - void writeDefaultConfiguration( - - ) { - - } - - @BuildStep - RuntimeInitializedClassBuildItem runtimeInitializedClass() { - return new RuntimeInitializedClassBuildItem(InetRunTime.class.getName()); - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index a7db493d10a0f..149ed2204eaee 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -5,11 +5,14 @@ import java.io.File; import java.lang.reflect.Modifier; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.graalvm.nativeimage.ImageInfo; +import org.jboss.logging.Logger; import io.quarkus.builder.Version; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; @@ -18,6 +21,8 @@ import io.quarkus.deployment.builditem.ApplicationClassNameBuildItem; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.builditem.BytecodeRecorderObjectLoaderBuildItem; +import io.quarkus.deployment.builditem.ConfigurationBuildItem; +import io.quarkus.deployment.builditem.ConfigurationTypeBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.JavaLibraryPathAdditionalPathBuildItem; @@ -25,13 +30,17 @@ import io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.MainClassBuildItem; import io.quarkus.deployment.builditem.ObjectSubstitutionBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.SslTrustStoreSystemPropertyBuildItem; import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import io.quarkus.deployment.configuration.BuildTimeConfigurationReader; +import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; import io.quarkus.deployment.recording.BytecodeRecorderImpl; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; @@ -50,6 +59,7 @@ class MainClassBuildStep { private static final String APP_CLASS = "io.quarkus.runner.ApplicationImpl"; private static final String MAIN_CLASS = "io.quarkus.runner.GeneratedMain"; private static final String STARTUP_CONTEXT = "STARTUP_CONTEXT"; + private static final String LOG = "LOG"; private static final String JAVA_LIBRARY_PATH = "java.library.path"; private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore"; @@ -65,7 +75,24 @@ MainClassBuildItem build(List staticInitTasks, List loaders, BuildProducer generatedClass, LaunchModeBuildItem launchMode, - ApplicationInfoBuildItem applicationInfo) { + ApplicationInfoBuildItem applicationInfo, + List runTimeDefaults, + List typeItems, + ConfigurationBuildItem configItem) { + + BuildTimeConfigurationReader.ReadResult readResult = configItem.getReadResult(); + Map defaults = new HashMap<>(); + for (RunTimeConfigurationDefaultBuildItem item : runTimeDefaults) { + if (defaults.putIfAbsent(item.getKey(), item.getValue()) != null) { + throw new IllegalStateException("More than one default value for " + item.getKey() + " was produced"); + } + } + List> additionalConfigTypes = typeItems.stream().map(ConfigurationTypeBuildItem::getValueType) + .collect(Collectors.toList()); + + ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClass, true); + + RunTimeConfigurationGenerator.generate(readResult, classOutput, defaults, additionalConfigTypes); appClassNameProducer.produce(new ApplicationClassNameBuildItem(APP_CLASS)); @@ -76,6 +103,9 @@ MainClassBuildItem build(List staticInitTasks, // Application class: static init + // LOG static field + FieldCreator logField = file.getFieldCreator(LOG, Logger.class).setModifiers(Modifier.STATIC); + FieldCreator scField = file.getFieldCreator(STARTUP_CONTEXT, StartupContext.class); scField.setModifiers(Modifier.STATIC); @@ -89,6 +119,14 @@ MainClassBuildItem build(List staticInitTasks, } mv.invokeStaticMethod(MethodDescriptor.ofMethod(Timing.class, "staticInitStarted", void.class)); + + // ensure that the config class is initialized + mv.invokeStaticMethod(RunTimeConfigurationGenerator.C_ENSURE_INITIALIZED); + + // Init the LOG instance + mv.writeStaticField(logField.getFieldDescriptor(), mv.invokeStaticMethod( + ofMethod(Logger.class, "getLogger", Logger.class, String.class), mv.load("io.quarkus.application"))); + ResultHandle startupContext = mv.newInstance(ofConstructor(StartupContext.class)); mv.writeStaticField(scField.getFieldDescriptor(), startupContext); TryBlock tryBlock = mv.tryBlock(); @@ -170,7 +208,7 @@ MainClassBuildItem build(List staticInitTasks, tryBlock = mv.tryBlock(); // Load the run time configuration - tryBlock.invokeStaticMethod(ConfigurationSetup.CREATE_RUN_TIME_CONFIG); + tryBlock.invokeStaticMethod(RunTimeConfigurationGenerator.C_CREATE_RUN_TIME_CONFIG); for (MainBytecodeRecorderBuildItem holder : mainMethod) { final BytecodeRecorderImpl recorder = holder.getBytecodeRecorder(); @@ -195,6 +233,8 @@ MainClassBuildItem build(List staticInitTasks, .map(f -> f.getInfo()) .sorted() .collect(Collectors.joining(", "))); + ResultHandle activeProfile = tryBlock + .invokeStaticMethod(ofMethod(ProfileManager.class, "getActiveProfile", String.class)); tryBlock.invokeStaticMethod( ofMethod(Timing.class, "printStartupTime", void.class, String.class, String.class, String.class, String.class, String.class, boolean.class), @@ -202,11 +242,13 @@ MainClassBuildItem build(List staticInitTasks, tryBlock.load(applicationInfo.getVersion()), tryBlock.load(Version.getVersion()), featuresHandle, - tryBlock.load(ProfileManager.getActiveProfile()), + activeProfile, tryBlock.load(LaunchMode.DEVELOPMENT.equals(launchMode.getLaunchMode()))); cb = tryBlock.addCatch(Throwable.class); - cb.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cb.getCaughtException()); + cb.invokeVirtualMethod(ofMethod(Logger.class, "error", void.class, Object.class, Throwable.class), + cb.readStaticField(logField.getFieldDescriptor()), cb.load("Failed to start application"), + cb.getCaughtException()); cb.invokeVirtualMethod(ofMethod(StartupContext.class, "close", void.class), startupContext); cb.throwException(RuntimeException.class, "Failed to start quarkus", cb.getCaughtException()); mv.returnValue(null); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageAutoFeatureStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageAutoFeatureStep.java index 794114ce1b76d..004f70ef28c06 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageAutoFeatureStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageAutoFeatureStep.java @@ -2,30 +2,40 @@ import static io.quarkus.gizmo.MethodDescriptor.ofMethod; +import java.io.File; +import java.io.IOException; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Enumeration; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; import org.graalvm.nativeimage.hosted.RuntimeReflection; -import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.DeploymentClassLoaderBuildItem; import io.quarkus.deployment.builditem.GeneratedNativeImageClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; @@ -33,6 +43,7 @@ import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.builditem.nativeimage.UnsafeAccessedFieldBuildItem; +import io.quarkus.gizmo.AssignableResultHandle; import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; @@ -47,15 +58,44 @@ public class NativeImageAutoFeatureStep { private static final String GRAAL_AUTOFEATURE = "io/quarkus/runner/AutoFeature"; private static final MethodDescriptor IMAGE_SINGLETONS_LOOKUP = ofMethod(ImageSingletons.class, "lookup", Object.class, Class.class); - private static final MethodDescriptor INITIALIZE_AT_RUN_TIME = ofMethod(RuntimeClassInitializationSupport.class, - "initializeAtRunTime", void.class, Class.class, String.class); - private static final MethodDescriptor RERUN_INITIALIZATION = ofMethod(RuntimeClassInitializationSupport.class, + private static final MethodDescriptor INITIALIZE_AT_RUN_TIME = ofMethod(RuntimeClassInitialization.class, + "initializeAtRunTime", void.class, Class[].class); + private static final MethodDescriptor RERUN_INITIALIZATION = ofMethod( + "org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport", "rerunInitialization", void.class, Class.class, String.class); static final String RUNTIME_REFLECTION = RuntimeReflection.class.getName(); static final String BEFORE_ANALYSIS_ACCESS = Feature.BeforeAnalysisAccess.class.getName(); static final String DYNAMIC_PROXY_REGISTRY = "com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry"; + static final String LOCALIZATION_FEATURE = "com.oracle.svm.core.jdk.LocalizationFeature"; + // TODO: Delete the following line when Quarkus no longer supports GraalVM 19.2.1. static final String LOCALIZATION_SUPPORT = "com.oracle.svm.core.jdk.LocalizationSupport"; + @BuildStep + List registerPackageResources( + List nativeImageResourceDirectories, + DeploymentClassLoaderBuildItem classLoader) + throws IOException, URISyntaxException { + List resources = new ArrayList<>(); + + for (NativeImageResourceDirectoryBuildItem nativeImageResourceDirectory : nativeImageResourceDirectories) { + String path = classLoader.getClassLoader().getResource(nativeImageResourceDirectory.getPath()).getPath(); + File resourceFile = Paths.get(new URL(path.substring(0, path.indexOf("!"))).toURI()).toFile(); + try (JarFile jarFile = new JarFile(resourceFile)) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String resourceName = entry.getName(); + if (!entry.isDirectory() && resourceName.startsWith(nativeImageResourceDirectory.getPath()) + && !resourceName.endsWith(".class")) { + resources.add(new NativeImageResourceBuildItem(resourceName)); + } + } + } + } + + return resources; + } + @BuildStep void generateFeature(BuildProducer nativeImageClass, List runtimeInitializedClassBuildItems, @@ -98,39 +138,37 @@ public void write(String s, byte[] bytes) { cc.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cc.getCaughtException()); } - ResultHandle initSingleton = overallCatch.invokeStaticMethod(IMAGE_SINGLETONS_LOOKUP, - overallCatch.loadClass(RuntimeClassInitializationSupport.class)); - ResultHandle quarkus = overallCatch.load("Quarkus"); - if (!runtimeInitializedClassBuildItems.isEmpty()) { ResultHandle thisClass = overallCatch.loadClass(GRAAL_AUTOFEATURE); ResultHandle cl = overallCatch.invokeVirtualMethod(ofMethod(Class.class, "getClassLoader", ClassLoader.class), thisClass); - for (String i : runtimeInitializedClassBuildItems.stream().map(RuntimeInitializedClassBuildItem::getClassName) - .collect(Collectors.toList())) { + ResultHandle classes = overallCatch.newArray(Class.class, + overallCatch.load(runtimeInitializedClassBuildItems.size())); + for (int i = 0; i < runtimeInitializedClassBuildItems.size(); i++) { TryBlock tc = overallCatch.tryBlock(); ResultHandle clazz = tc.invokeStaticMethod( ofMethod(Class.class, "forName", Class.class, String.class, boolean.class, ClassLoader.class), - tc.load(i), tc.load(false), cl); - tc.invokeInterfaceMethod(INITIALIZE_AT_RUN_TIME, initSingleton, clazz, quarkus); - + tc.load(runtimeInitializedClassBuildItems.get(i).getClassName()), tc.load(false), cl); + tc.writeArrayValue(classes, i, clazz); CatchBlockCreator cc = tc.addCatch(Throwable.class); cc.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cc.getCaughtException()); } - + overallCatch.invokeStaticMethod(INITIALIZE_AT_RUN_TIME, classes); } // hack in reinitialization of process info classes - { + if (!runtimeReinitializedClassBuildItems.isEmpty()) { ResultHandle thisClass = overallCatch.loadClass(GRAAL_AUTOFEATURE); ResultHandle cl = overallCatch.invokeVirtualMethod(ofMethod(Class.class, "getClassLoader", ClassLoader.class), thisClass); - for (String i : runtimeReinitializedClassBuildItems.stream().map(RuntimeReinitializedClassBuildItem::getClassName) - .collect(Collectors.toList())) { + ResultHandle initSingleton = overallCatch.invokeStaticMethod(IMAGE_SINGLETONS_LOOKUP, + overallCatch.loadClass("org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport")); + ResultHandle quarkus = overallCatch.load("Quarkus"); + for (RuntimeReinitializedClassBuildItem runtimeReinitializedClass : runtimeReinitializedClassBuildItems) { TryBlock tc = overallCatch.tryBlock(); ResultHandle clazz = tc.invokeStaticMethod( ofMethod(Class.class, "forName", Class.class, String.class, boolean.class, ClassLoader.class), - tc.load(i), tc.load(false), cl); + tc.load(runtimeReinitializedClass.getClassName()), tc.load(false), cl); tc.invokeInterfaceMethod(RERUN_INITIALIZATION, initSingleton, clazz, quarkus); CatchBlockCreator cc = tc.addCatch(Throwable.class); @@ -170,7 +208,26 @@ public void write(String s, byte[] bytes) { } if (!resourceBundles.isEmpty()) { - ResultHandle locClass = overallCatch.loadClass(LOCALIZATION_SUPPORT); + /* + * Start of a temporary workaround to support both GraalVM 19.2.1 and 19.3.1 at the same time. + * TODO: Delete this workaround when Quarkus no longer supports GraalVM 19.2.1. + */ + AssignableResultHandle locClass = overallCatch.createVariable(Class.class); + TryBlock workaroundTryBlock = overallCatch.tryBlock(); + workaroundTryBlock.assign(locClass, workaroundTryBlock.loadClass(LOCALIZATION_FEATURE)); + // The following line is required to throw an exception and make sure we load the 19.2.1 class when needed. + workaroundTryBlock.invokeVirtualMethod( + ofMethod(Class.class, "getDeclaredMethod", Method.class, String.class, Class[].class), locClass, + workaroundTryBlock.load("addBundleToCache"), + workaroundTryBlock.marshalAsArray(Class.class, workaroundTryBlock.loadClass(String.class))); + CatchBlockCreator workaroundCatchBlock = workaroundTryBlock.addCatch(Throwable.class); + workaroundCatchBlock.assign(locClass, workaroundCatchBlock.loadClass(LOCALIZATION_SUPPORT)); + /* + * End of the temporary workaround. + */ + + // TODO: Uncomment the following line when the temporary workaround above is deleted. + //ResultHandle locClass = overallCatch.loadClass(LOCALIZATION_FEATURE); ResultHandle params = overallCatch.marshalAsArray(Class.class, overallCatch.loadClass(String.class)); ResultHandle registerMethod = overallCatch.invokeVirtualMethod( diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java index 05413732fca39..130df595a8476 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageConfigBuildStep.java @@ -18,7 +18,8 @@ import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.JavaLibraryPathAdditionalPathBuildItem; import io.quarkus.deployment.builditem.JniBuildItem; -import io.quarkus.deployment.builditem.NativeEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllTimeZonesBuildItem; import io.quarkus.deployment.builditem.SslNativeConfigBuildItem; import io.quarkus.deployment.builditem.SslTrustStoreSystemPropertyBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; @@ -43,7 +44,8 @@ void build(SslContextConfigurationRecorder sslContextConfigurationRecorder, List nativeImageConfigBuildItems, SslNativeConfigBuildItem sslNativeConfig, List jniBuildItems, - List nativeEnableAllCharsetsBuildItems, + List nativeImageEnableAllCharsetsBuildItems, + List nativeImageEnableAllTimeZonesBuildItems, List extensionSslNativeSupport, List enableAllSecurityServicesBuildItems, BuildProducer proxy, @@ -102,7 +104,8 @@ void build(SslContextConfigurationRecorder sslContextConfigurationRecorder, } else { // On MacOS, the SunEC library is directly in jre/lib/ // This is useful for testing or if you have a similar environment in production - systemProperty.produce(new SystemPropertyBuildItem("java.library.path", graalVmLibDirectory.toString())); + javaLibraryPathAdditionalPath + .produce(new JavaLibraryPathAdditionalPathBuildItem(graalVmLibDirectory.toString())); } // This is useful for testing but the user will have to override it. @@ -137,9 +140,13 @@ void build(SslContextConfigurationRecorder sslContextConfigurationRecorder, nativeImage.produce(new NativeImageSystemPropertyBuildItem("quarkus.jni.enable", "true")); } - if (!nativeEnableAllCharsetsBuildItems.isEmpty()) { + if (!nativeImageEnableAllCharsetsBuildItems.isEmpty()) { nativeImage.produce(new NativeImageSystemPropertyBuildItem("quarkus.native.enable-all-charsets", "true")); } + + if (!nativeImageEnableAllTimeZonesBuildItems.isEmpty()) { + nativeImage.produce(new NativeImageSystemPropertyBuildItem("quarkus.native.enable-all-timezones", "true")); + } } private Boolean isSslNativeEnabled(SslNativeConfigBuildItem sslNativeConfig, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java index eac766828b33e..f8c1cc48e64a7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ReflectiveHierarchyStep.java @@ -28,6 +28,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassFinalFieldsWritablePredicateBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; @@ -47,12 +48,25 @@ public class ReflectiveHierarchyStep { @Inject List ignored; + @Inject + List finalFieldsWritablePredicates; + @BuildStep public void build() throws Exception { Set processedReflectiveHierarchies = new HashSet<>(); Set unindexedClasses = new TreeSet<>(); + + Predicate finalFieldsWritable = (c) -> false; // no need to make final fields writable by default + if (!finalFieldsWritablePredicates.isEmpty()) { + // create a predicate that returns true if any of the predicates says that final fields need to be writable + finalFieldsWritable = finalFieldsWritablePredicates + .stream() + .map(ReflectiveClassFinalFieldsWritablePredicateBuildItem::getPredicate) + .reduce(c -> false, Predicate::or); + } + for (ReflectiveHierarchyBuildItem i : hierarchy) { - addReflectiveHierarchy(i, i.getType(), processedReflectiveHierarchies, unindexedClasses); + addReflectiveHierarchy(i, i.getType(), processedReflectiveHierarchies, unindexedClasses, finalFieldsWritable); } for (ReflectiveHierarchyIgnoreWarningBuildItem i : ignored) { unindexedClasses.remove(i.getDotName()); @@ -69,7 +83,8 @@ public void build() throws Exception { } private void addReflectiveHierarchy(ReflectiveHierarchyBuildItem reflectiveHierarchyBuildItem, Type type, - Set processedReflectiveHierarchies, Set unindexedClasses) { + Set processedReflectiveHierarchies, Set unindexedClasses, + Predicate finalFieldsWritable) { if (type instanceof VoidType || type instanceof PrimitiveType || type instanceof UnresolvedTypeVariable) { @@ -79,47 +94,55 @@ private void addReflectiveHierarchy(ReflectiveHierarchyBuildItem reflectiveHiera return; } - addClassTypeHierarchy(reflectiveHierarchyBuildItem, type.name(), processedReflectiveHierarchies, unindexedClasses); + addClassTypeHierarchy(reflectiveHierarchyBuildItem, type.name(), processedReflectiveHierarchies, unindexedClasses, + finalFieldsWritable); for (ClassInfo subclass : combinedIndexBuildItem.getIndex().getAllKnownSubclasses(type.name())) { addClassTypeHierarchy(reflectiveHierarchyBuildItem, subclass.name(), processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); } for (ClassInfo subclass : combinedIndexBuildItem.getIndex().getAllKnownImplementors(type.name())) { addClassTypeHierarchy(reflectiveHierarchyBuildItem, subclass.name(), processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); } } else if (type instanceof ArrayType) { addReflectiveHierarchy(reflectiveHierarchyBuildItem, type.asArrayType().component(), processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); } else if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; if (!reflectiveHierarchyBuildItem.getIgnorePredicate().test(parameterizedType.name())) { addClassTypeHierarchy(reflectiveHierarchyBuildItem, parameterizedType.name(), processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); } for (Type typeArgument : parameterizedType.arguments()) { addReflectiveHierarchy(reflectiveHierarchyBuildItem, typeArgument, processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); } } } private void addClassTypeHierarchy(ReflectiveHierarchyBuildItem reflectiveHierarchyBuildItem, DotName name, Set processedReflectiveHierarchies, - Set unindexedClasses) { + Set unindexedClasses, Predicate finalFieldsWritable) { if (skipClass(name, reflectiveHierarchyBuildItem.getIgnorePredicate(), processedReflectiveHierarchies)) { return; } processedReflectiveHierarchies.add(name); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, name.toString())); + ClassInfo info = (reflectiveHierarchyBuildItem.getIndex() != null ? reflectiveHierarchyBuildItem.getIndex() : combinedIndexBuildItem.getIndex()).getClassByName(name); + reflectiveClass.produce( + ReflectiveClassBuildItem + .builder(name.toString()) + .methods(true) + .fields(true) + .finalFieldsWritable(doFinalFieldsNeedToBeWritable(info, finalFieldsWritable)) + .build()); if (info == null) { unindexedClasses.add(name); } else { addClassTypeHierarchy(reflectiveHierarchyBuildItem, info.superName(), processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); for (FieldInfo field : info.fields()) { if (Modifier.isStatic(field.flags()) || field.name().startsWith("this$") || field.name().startsWith("val$")) { // skip the static fields (especially loggers) @@ -127,7 +150,7 @@ private void addClassTypeHierarchy(ReflectiveHierarchyBuildItem reflectiveHierar continue; } addReflectiveHierarchy(reflectiveHierarchyBuildItem, field.type(), processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); } for (MethodInfo method : info.methods()) { if (method.parameters().size() > 0 || Modifier.isStatic(method.flags()) @@ -136,7 +159,7 @@ private void addClassTypeHierarchy(ReflectiveHierarchyBuildItem reflectiveHierar continue; } addReflectiveHierarchy(reflectiveHierarchyBuildItem, method.returnType(), processedReflectiveHierarchies, - unindexedClasses); + unindexedClasses, finalFieldsWritable); } } } @@ -144,4 +167,11 @@ private void addClassTypeHierarchy(ReflectiveHierarchyBuildItem reflectiveHierar private boolean skipClass(DotName name, Predicate ignorePredicate, Set processedReflectiveHierarchies) { return ignorePredicate.test(name) || processedReflectiveHierarchies.contains(name); } + + private boolean doFinalFieldsNeedToBeWritable(ClassInfo classInfo, Predicate finalFieldsWritable) { + if (classInfo == null) { + return false; + } + return finalFieldsWritable.test(classInfo); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ResourceBundleStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ResourceBundleStep.java new file mode 100644 index 0000000000000..f2cfba72a2ce0 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ResourceBundleStep.java @@ -0,0 +1,17 @@ +package io.quarkus.deployment.steps; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; + +public class ResourceBundleStep { + + @BuildStep + public NativeImageResourceBundleBuildItem nativeImageResourceBundle() { + /* + * The following resource bundle sometimes needs to be included into the native image with JDK 11. + * This might no longer be required if GraalVM auto-includes it in a future release. + * See https://github.com/oracle/graal/issues/2005 for more details about it. + */ + return new NativeImageResourceBundleBuildItem("sun.security.util.Resources"); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ClassOutputUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ClassOutputUtil.java index d89e509a61dbe..80e75a334642f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/ClassOutputUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ClassOutputUtil.java @@ -5,7 +5,7 @@ import java.nio.file.Files; /** - * Utility that dumps bytes from a class to a file - useful for debugging generated clases + * Utility that dumps bytes from a class to a file - useful for debugging generated classes */ public final class ClassOutputUtil { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java index cafb22c254750..24607ced1447e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java @@ -8,6 +8,9 @@ import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class FileUtil { @@ -48,4 +51,30 @@ public static byte[] readFileContents(InputStream inputStream) throws IOExceptio } return out.toByteArray(); } + + /** + * Translates a file path from the Windows Style to a syntax accepted by Docker, + * so that volumes be safely mounted in both Docker for Windows and the legacy + * Docker Toolbox. + *

+ * docker run -v //c/foo/bar:/somewhere (...) + *

+ * You should only use this method on Windows-style paths, and not Unix-style + * paths. + * + * @see https://github.com/quarkusio/quarkus/issues/5360 + * @param windowsStylePath A path formatted in Windows-style, e.g. "C:\foo\bar". + * @return A translated path accepted by Docker, e.g. "//c/foo/bar". + */ + public static String translateToVolumePath(String windowsStylePath) { + String translated = windowsStylePath.replace('\\', '/'); + Pattern p = Pattern.compile("^(\\w)(?:$|:(/)?(.*))"); + Matcher m = p.matcher(translated); + if (m.matches()) { + String slash = Optional.ofNullable(m.group(2)).orElse("/"); + String path = Optional.ofNullable(m.group(3)).orElse(""); + return "//" + m.group(1).toLowerCase() + slash + path; + } + return translated; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java index aa6192c89a905..ae4fd3fe1aa42 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java @@ -6,6 +6,7 @@ import java.util.LinkedList; import java.util.List; +import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; @@ -63,46 +64,80 @@ public static List resolveTypeParameters(DotName input, DotName target, In if (recursiveMatchResult == null) { return Collections.emptyList(); } + final List result = resolveTypeParameters(recursiveMatchResult); + + if (result.size() != recursiveMatchResult.argumentsOfMatch.size()) { + throw new IllegalStateException("Unable to properly match generic types"); + } + + return result; + } + + private static List resolveTypeParameters(RecursiveMatchResult recursiveMatchResult) { final List result = new ArrayList<>(); for (int i = 0; i < recursiveMatchResult.argumentsOfMatch.size(); i++) { final Type argument = recursiveMatchResult.argumentsOfMatch.get(i); - if (isDirectlyHandledType(argument)) { + if (argument instanceof ClassType) { result.add(argument); - } else if (argument instanceof TypeVariable) { - - String unmatchedParameter = argument.asTypeVariable().identifier(); - - for (RecursiveMatchLevel recursiveMatchLevel : recursiveMatchResult.recursiveMatchLevels) { - Type matchingCapturedType = null; - for (int j = 0; j < recursiveMatchLevel.definitions.size(); j++) { - final Type definition = recursiveMatchLevel.definitions.get(j); - if ((definition instanceof TypeVariable) - && unmatchedParameter.equals(definition.asTypeVariable().identifier())) { - matchingCapturedType = recursiveMatchLevel.captures.get(j); - break; // out of the definitions loop - } - } - // at this point their MUST be a match, if there isn't we have made some mistake in the implementation - if (matchingCapturedType == null) { - throw new IllegalStateException("Error retrieving generic types"); + } else if (argument instanceof ParameterizedType) { + ParameterizedType argumentParameterizedType = argument.asParameterizedType(); + List resolvedTypes = new ArrayList<>(argumentParameterizedType.arguments().size()); + for (Type argType : argumentParameterizedType.arguments()) { + if (argType instanceof TypeVariable) { + resolvedTypes.add(findTypeFromTypeVariable(recursiveMatchResult, argType.asTypeVariable())); + } else { + resolvedTypes.add(argType); } - if (isDirectlyHandledType(matchingCapturedType)) { - result.add(matchingCapturedType); - break; // out of level loop - } - if (matchingCapturedType instanceof TypeVariable) { - // continue the search in the lower levels using the new name - unmatchedParameter = matchingCapturedType.asTypeVariable().identifier(); + } + result.add(ParameterizedType.create(argumentParameterizedType.name(), resolvedTypes.toArray(new Type[0]), + argumentParameterizedType.owner())); + } else if (argument instanceof TypeVariable) { + Type typeFromTypeVariable = findTypeFromTypeVariable(recursiveMatchResult, argument.asTypeVariable()); + if (typeFromTypeVariable != null) { + result.add(typeFromTypeVariable); + } + } else if (argument instanceof ArrayType) { + ArrayType argumentAsArrayType = argument.asArrayType(); + Type componentType = argumentAsArrayType.component(); + if (componentType instanceof TypeVariable) { // should always be the case + Type typeFromTypeVariable = findTypeFromTypeVariable(recursiveMatchResult, componentType.asTypeVariable()); + if (typeFromTypeVariable != null) { + result.add(ArrayType.create(typeFromTypeVariable, argumentAsArrayType.dimensions())); } } } } + return result; + } - if (result.size() != recursiveMatchResult.argumentsOfMatch.size()) { - throw new IllegalStateException("Unable to properly match generic types"); + private static Type findTypeFromTypeVariable(RecursiveMatchResult recursiveMatchResult, TypeVariable typeVariable) { + String unmatchedParameter = typeVariable.identifier(); + + for (RecursiveMatchLevel recursiveMatchLevel : recursiveMatchResult.recursiveMatchLevels) { + Type matchingCapturedType = null; + for (int j = 0; j < recursiveMatchLevel.definitions.size(); j++) { + final Type definition = recursiveMatchLevel.definitions.get(j); + if ((definition instanceof TypeVariable) + && unmatchedParameter.equals(definition.asTypeVariable().identifier())) { + matchingCapturedType = recursiveMatchLevel.captures.get(j); + break; // out of the definitions loop + } + } + // at this point their MUST be a match, if there isn't we have made some mistake in the implementation + if (matchingCapturedType == null) { + throw new IllegalStateException("Error retrieving generic types"); + } + if (isDirectlyHandledType(matchingCapturedType)) { + // search is over + return matchingCapturedType; + } + if (matchingCapturedType instanceof TypeVariable) { + // continue the search in the lower levels using the new name + unmatchedParameter = matchingCapturedType.asTypeVariable().identifier(); + } } - return result; + return null; } private static boolean isDirectlyHandledType(Type matchingCapturedType) { @@ -172,6 +207,19 @@ private static List addArgumentIfNeeded(Type type, IndexVie final RecursiveMatchLevel recursiveMatchLevel = new RecursiveMatchLevel(classInfo.typeParameters(), type.asParameterizedType().arguments()); newVisitedTypes.add(0, recursiveMatchLevel); + } else if (type instanceof ClassType) { + // we need to check if the class contains any bounds + final ClassInfo classInfo = index.getClassByName(type.name()); + if ((classInfo != null) && !classInfo.typeParameters().isEmpty()) { + // in this case we just use the first bound as the capture type + // TODO do we need something more sophisticated than this? + List captures = new ArrayList<>(classInfo.typeParameters().size()); + for (TypeVariable typeParameter : classInfo.typeParameters()) { + captures.add(typeParameter.bounds().get(0)); + } + final RecursiveMatchLevel recursiveMatchLevel = new RecursiveMatchLevel(classInfo.typeParameters(), captures); + newVisitedTypes.add(0, recursiveMatchLevel); + } } return newVisitedTypes; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ReflectUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ReflectUtil.java index 0a65c6f706d99..3381cc95ebc7b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/ReflectUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ReflectUtil.java @@ -1,10 +1,16 @@ package io.quarkus.deployment.util; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -74,6 +80,24 @@ public static Class rawTypeOf(final Type type) { } } + private static final Class[] NO_CLASSES = new Class[0]; + + public static Class[] rawTypesOfDestructive(final Type[] types) { + if (types.length == 0) { + return NO_CLASSES; + } + Type t; + Class r; + for (int i = 0; i < types.length; i++) { + t = types[i]; + r = rawTypeOf(t); + if (r != t) { + types[i] = r; + } + } + return Arrays.copyOf(types, types.length, Class[].class); + } + public static Type typeOfParameter(final Type type, final int paramIdx) { if (type instanceof ParameterizedType) { return ((ParameterizedType) type).getActualTypeArguments()[paramIdx]; @@ -94,6 +118,26 @@ public static void setFieldVal(Field field, Object obj, Object value) { } } + public static T newInstance(Class clazz) { + try { + return clazz.getConstructor().newInstance(); + } catch (InstantiationException e) { + throw toError(e); + } catch (InvocationTargetException e) { + try { + throw e.getCause(); + } catch (RuntimeException | Error e2) { + throw e2; + } catch (Throwable t) { + throw new UndeclaredThrowableException(t); + } + } catch (NoSuchMethodException e) { + throw toError(e); + } catch (IllegalAccessException e) { + throw toError(e); + } + } + public static InstantiationError toError(final InstantiationException e) { final InstantiationError error = new InstantiationError(e.getMessage()); error.setStackTrace(e.getStackTrace()); @@ -117,4 +161,27 @@ public static NoSuchFieldError toError(final NoSuchFieldException e) { error.setStackTrace(e.getStackTrace()); return error; } + + public static UndeclaredThrowableException unwrapInvocationTargetException(InvocationTargetException original) { + try { + throw original.getCause(); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + return new UndeclaredThrowableException(t); + } + } + + public static IllegalArgumentException reportError(AnnotatedElement e, String fmt, Object... args) { + if (e instanceof Member) { + return new IllegalArgumentException( + String.format(fmt, args) + " at " + e + " of " + ((Member) e).getDeclaringClass()); + } else if (e instanceof Parameter) { + return new IllegalArgumentException( + String.format(fmt, args) + " at " + e + " of " + ((Parameter) e).getDeclaringExecutable() + " of " + + ((Parameter) e).getDeclaringExecutable().getDeclaringClass()); + } else { + return new IllegalArgumentException(String.format(fmt, args) + " at " + e); + } + } } diff --git a/core/deployment/src/main/java/io/quarkus/runner/RuntimeClassLoader.java b/core/deployment/src/main/java/io/quarkus/runner/RuntimeClassLoader.java index 6885d2e8d9be1..cc07cb92f2643 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/RuntimeClassLoader.java +++ b/core/deployment/src/main/java/io/quarkus/runner/RuntimeClassLoader.java @@ -8,12 +8,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.net.MalformedURLException; import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.CodeSource; @@ -30,7 +32,6 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -42,7 +43,6 @@ import org.objectweb.asm.ClassWriter; import io.quarkus.deployment.ClassOutput; -import io.quarkus.deployment.QuarkusClassWriter; public class RuntimeClassLoader extends ClassLoader implements ClassOutput, TransformerTarget { @@ -54,6 +54,7 @@ public class RuntimeClassLoader extends ClassLoader implements ClassOutput, Tran private final Map resources = new ConcurrentHashMap<>(); private volatile Map>> bytecodeTransformers = null; + private volatile ClassLoader transformerSafeClassLoader; private final List applicationClassDirectories; @@ -66,7 +67,7 @@ public class RuntimeClassLoader extends ClassLoader implements ClassOutput, Tran private static final String DEBUG_CLASSES_DIR = System.getProperty("quarkus.debug.generated-classes-dir"); - private final ConcurrentHashMap>> loadingClasses = new ConcurrentHashMap<>(); + private final ConcurrentHashMap loadingClasses = new ConcurrentHashMap<>(); static { registerAsParallelCapable(); @@ -205,11 +206,15 @@ protected Class findClass(String name) throws ClassNotFoundException { Path classLoc = getClassInApplicationClassPaths(name); if (classLoc != null) { - CompletableFuture> res = new CompletableFuture<>(); - Future> loadingClass = loadingClasses.putIfAbsent(name, res); + LoadingClass res = new LoadingClass(new CompletableFuture<>(), Thread.currentThread()); + LoadingClass loadingClass = loadingClasses.putIfAbsent(name, res); if (loadingClass != null) { + if (loadingClass.initiator == Thread.currentThread()) { + throw new LinkageError( + "Load caused recursion in RuntimeClassLoader, this is a Quarkus bug loading class: " + name); + } try { - return loadingClass.get(); + return loadingClass.value.get(); } catch (Exception e) { throw new ClassNotFoundException("Failed to load " + name, e); } @@ -223,13 +228,13 @@ protected Class findClass(String name) throws ClassNotFoundException { bytes = handleTransform(name, bytes); definePackage(name); Class clazz = defineClass(name, bytes, 0, bytes.length, defaultProtectionDomain); - res.complete(clazz); + res.value.complete(clazz); return clazz; } catch (RuntimeException e) { - res.completeExceptionally(e); + res.value.completeExceptionally(e); throw e; } catch (Throwable e) { - res.completeExceptionally(e); + res.value.completeExceptionally(e); throw e; } } @@ -249,6 +254,7 @@ public void writeClass(boolean applicationClass, String className, byte[] data) debugPath.mkdir(); } File classFile = new File(debugPath, dotName + ".class"); + classFile.getParentFile().mkdirs(); try (FileOutputStream classWriter = new FileOutputStream(classFile)) { classWriter.write(data); } @@ -276,9 +282,29 @@ public void writeClass(boolean applicationClass, String className, byte[] data) } } + @Override + public Writer writeSource(final String className) { + if (DEBUG_CLASSES_DIR != null) { + try { + File debugPath = new File(DEBUG_CLASSES_DIR); + if (!debugPath.exists()) { + debugPath.mkdir(); + } + File classFile = new File(debugPath, className + ".zig"); + classFile.getParentFile().mkdirs(); + log.infof("Wrote %s", classFile.getAbsolutePath()); + return new OutputStreamWriter(new FileOutputStream(classFile), StandardCharsets.UTF_8); + } catch (Throwable t) { + t.printStackTrace(); + } + } + return ClassOutput.super.writeSource(className); + } + @Override public void setTransformers(Map>> functions) { this.bytecodeTransformers = functions; + this.transformerSafeClassLoader = Thread.currentThread().getContextClassLoader(); } public void setApplicationArchives(List archives) { @@ -400,7 +426,12 @@ private byte[] handleTransform(String name, byte[] bytes) { } ClassReader cr = new ClassReader(bytes); - ClassWriter writer = new QuarkusClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + ClassWriter writer = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { + @Override + protected ClassLoader getClassLoader() { + return transformerSafeClassLoader; + } + }; ClassVisitor visitor = writer; for (BiFunction i : transformers) { visitor = i.apply(name, visitor); @@ -437,7 +468,13 @@ private Path getClassInApplicationClassPaths(String name) { private URL findApplicationResource(String name) { Path resourcePath = null; - + // Resource names are always separated by the "/" character. + // Here we are trying to resolve those resources using a filesystem + // Path, so we replace the "/" character with the filesystem + // specific separator before resolving + if (File.separatorChar != '/') { + name = name.replace('/', File.separatorChar); + } for (Path i : applicationClassDirectories) { resourcePath = i.resolve(name); if (Files.exists(resourcePath)) { @@ -445,8 +482,7 @@ private URL findApplicationResource(String name) { } } try { - return resourcePath != null && Files.exists(resourcePath) ? resourcePath.toUri() - .toURL() : null; + return resourcePath != null && Files.exists(resourcePath) ? resourcePath.toUri().toURL() : null; } catch (MalformedURLException e) { throw new RuntimeException(e); } @@ -499,14 +535,24 @@ private ProtectionDomain createDefaultProtectionDomain(Path applicationClasspath URL url = null; if (applicationClasspath != null) { try { - URI uri = new URI("file", null, applicationClasspath.toString(), null); + URI uri = applicationClasspath.toUri(); url = uri.toURL(); - } catch (URISyntaxException | MalformedURLException e) { - log.error("URL codeSource location for path " + applicationClasspath + " could not be created."); + } catch (MalformedURLException e) { + log.error("URL codeSource location for path " + applicationClasspath + " could not be created.", e); } } CodeSource codesource = new CodeSource(url, (Certificate[]) null); ProtectionDomain protectionDomain = new ProtectionDomain(codesource, null, this, null); return protectionDomain; } + + static final class LoadingClass { + final CompletableFuture> value; + final Thread initiator; + + LoadingClass(CompletableFuture> value, Thread initiator) { + this.value = value; + this.initiator = initiator; + } + } } diff --git a/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java b/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java index bae45a1b5e308..1f81788a81db5 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java +++ b/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java @@ -29,9 +29,11 @@ import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.ApplicationClassNameBuildItem; import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; +import io.quarkus.deployment.builditem.DeploymentClassLoaderBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; +import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; import io.quarkus.runtime.Application; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ProfileManager; @@ -95,6 +97,7 @@ public void run() { builder.setRoot(target); builder.setClassLoader(loader); builder.setLaunchMode(launchMode); + builder.setBuildSystemProperties(buildSystemProperties); if (liveReloadState != null) { builder.setLiveReloadState(liveReloadState); } @@ -117,7 +120,14 @@ public void run() { functions.computeIfAbsent(i.getClassToTransform(), (f) -> new ArrayList<>()).add(i.getVisitorFunction()); } + DeploymentClassLoaderBuildItem deploymentClassLoaderBuildItem = result + .consume(DeploymentClassLoaderBuildItem.class); + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + + // make sure we use the DeploymentClassLoader for executing transformers since this is the only safe CL for transformations at this point + Thread.currentThread().setContextClassLoader(deploymentClassLoaderBuildItem.getClassLoader()); transformerTarget.setTransformers(functions); + Thread.currentThread().setContextClassLoader(previous); } if (loader instanceof RuntimeClassLoader) { @@ -133,12 +143,26 @@ public void run() { } final Application application; - Class appClass = loader - .loadClass(result.consume(ApplicationClassNameBuildItem.class).getClassName()) - .asSubclass(Application.class); + final String className = result.consume(ApplicationClassNameBuildItem.class).getClassName(); ClassLoader old = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(loader); + Class appClass; + try { + // force init here + appClass = Class.forName(className, true, loader).asSubclass(Application.class); + } catch (Throwable t) { + // todo: dev mode expects run time config to be available immediately even if static init didn't complete. + try { + final Class configClass = Class.forName(RunTimeConfigurationGenerator.CONFIG_CLASS_NAME, true, + loader); + configClass.getDeclaredMethod(RunTimeConfigurationGenerator.C_CREATE_RUN_TIME_CONFIG.getName()) + .invoke(null); + } catch (Throwable t2) { + t.addSuppressed(t2); + } + throw t; + } application = appClass.newInstance(); application.start(null); } finally { diff --git a/core/deployment/src/test/java/io/quarkus/deployment/util/FileUtilTest.java b/core/deployment/src/test/java/io/quarkus/deployment/util/FileUtilTest.java new file mode 100644 index 0000000000000..dcc09761c4738 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/util/FileUtilTest.java @@ -0,0 +1,28 @@ +package io.quarkus.deployment.util; + +import static io.quarkus.deployment.util.FileUtil.translateToVolumePath; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class FileUtilTest { + + @Test + public void testTranslateToVolumePath() { + // Windows-Style paths are formatted. + assertEquals("//c/", translateToVolumePath("C")); + assertEquals("//c/", translateToVolumePath("C:")); + assertEquals("//c/", translateToVolumePath("C:\\")); + assertEquals("//c/Users", translateToVolumePath("C:\\Users")); + assertEquals("//c/Users/Quarkus/lambdatest-1.0-SNAPSHOT-native-image-source-jar", + translateToVolumePath("C:\\Users\\Quarkus\\lambdatest-1.0-SNAPSHOT-native-image-source-jar")); + + // Side effect for Unix-style path. + assertEquals("//c/Users/Quarkus", translateToVolumePath("c:/Users/Quarkus")); + + // Side effects for fancy inputs - for the sake of documentation. + assertEquals("something/bizarre", translateToVolumePath("something\\bizarre")); + assertEquals("something.bizarre", translateToVolumePath("something.bizarre")); + } + +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java b/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java index dd164739ffdec..3a3294c097fae 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java @@ -50,121 +50,113 @@ public void testAbstractSingle() { @Test public void testSimplestImpl() { final Index index = index(Single.class, SingleImpl.class); - final DotName impl = DotName.createSimple(SingleImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(STRING); + checkRepoArg(index, SingleImpl.class, Single.class, String.class); } @Test public void testSimplestImplWithBound() { final Index index = index(SingleWithBound.class, SingleWithBoundImpl.class); - final DotName impl = DotName.createSimple(SingleWithBoundImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, - DotName.createSimple(SingleWithBound.class.getName()), index); - assertThat(result).extracting("name").containsOnly(DotName.createSimple(List.class.getName())); + checkRepoArg(index, SingleWithBoundImpl.class, SingleWithBound.class, List.class); } @Test public void testSimpleImplMultipleParams() { final Index index = index(Multiple.class, MultipleImpl.class); - final DotName impl = DotName.createSimple(MultipleImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, MULTIPLE, index); - assertThat(result).extracting("name").containsExactly(INTEGER, STRING); + checkRepoArg(index, MultipleImpl.class, Multiple.class, Integer.class, String.class); } @Test public void testInverseParameterNames() { final Index index = index(Multiple.class, InverseMultiple.class, InverseMultipleImpl.class); - final DotName impl = DotName.createSimple(InverseMultipleImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, MULTIPLE, index); - assertThat(result).extracting("name").containsExactly(DOUBLE, INTEGER); + checkRepoArg(index, InverseMultipleImpl.class, Multiple.class, Double.class, Integer.class); } @Test public void testImplExtendsSimplestImplementation() { final Index index = index(Single.class, SingleImpl.class, SingleImplImpl.class); - final DotName impl = DotName.createSimple(SingleImplImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(STRING); + checkRepoArg(index, SingleImplImpl.class, Single.class, String.class); } @Test public void testImplementationOfInterfaceThatExtendsSimpleWithoutParam() { final Index index = index(Single.class, ExtendsSimpleNoParam.class, ExtendsSimpleNoParamImpl.class); - final DotName impl = DotName.createSimple(ExtendsSimpleNoParamImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(DOUBLE); + checkRepoArg(index, ExtendsSimpleNoParamImpl.class, Single.class, Double.class); } @Test public void testImplExtendsImplOfInterfaceThatExtendsSimpleWithoutParams() { final Index index = index(Single.class, ExtendsSimpleNoParam.class, ExtendsSimpleNoParamImpl.class, ExtendsSimpleNoParamImplImpl.class); - final DotName impl = DotName.createSimple(ExtendsSimpleNoParamImplImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(DOUBLE); + checkRepoArg(index, ExtendsSimpleNoParamImplImpl.class, Single.class, Double.class); } @Test public void testImplOfInterfaceThatExtendsSimpleWithParam() { final Index index = index(Single.class, ExtendsSimpleWithParam.class, ExtendsSimpleWithParamImpl.class); - final DotName impl = DotName.createSimple(ExtendsSimpleWithParamImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(INTEGER); + checkRepoArg(index, ExtendsSimpleWithParamImpl.class, Single.class, Integer.class); } @Test public void testImplOfInterfaceThatExtendsSimpleWithParamInMultipleLevels() { final Index index = index(Single.class, ExtendsSimpleWithParam.class, ExtendsExtendsSimpleWithParam.class, ExtendsExtendsSimpleWithParamImpl.class); - final DotName impl = DotName.createSimple(ExtendsExtendsSimpleWithParamImpl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(DOUBLE); + checkRepoArg(index, ExtendsExtendsSimpleWithParamImpl.class, Single.class, Double.class); } @Test public void testImplOfInterfaceThatExtendsSimpleWithGenericParamInMultipleLevels() { final Index index = index(Single.class, ExtendsSimpleWithParam.class, ExtendsExtendsSimpleWithParam.class, ExtendsExtendsSimpleGenericParam.class); - final DotName impl = DotName.createSimple(ExtendsExtendsSimpleGenericParam.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(DotName.createSimple(Map.class.getName())); + checkRepoArg(index, ExtendsExtendsSimpleGenericParam.class, Single.class, Map.class); } @Test public void testImplOfMultipleWithParamsInDifferentLevels() { final Index index = index(Multiple.class, MultipleT1.class, ExtendsMultipleT1Impl.class); - final DotName impl = DotName.createSimple(ExtendsMultipleT1Impl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, MULTIPLE, index); - assertThat(result).extracting("name").containsOnly(INTEGER, STRING); + checkRepoArg(index, ExtendsMultipleT1Impl.class, Multiple.class, Integer.class, String.class); } @Test public void testImplOfAbstractMultipleWithParamsInDifferentLevels() { final Index index = index(Multiple.class, MultipleT1.class, AbstractMultipleT1Impl.class, ExtendsAbstractMultipleT1Impl.class); - final DotName impl = DotName.createSimple(ExtendsAbstractMultipleT1Impl.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, MULTIPLE, index); - assertThat(result).extracting("name").containsOnly(INTEGER, STRING); + checkRepoArg(index, ExtendsAbstractMultipleT1Impl.class, Multiple.class, Integer.class, String.class); } @Test public void testMultiplePathsToSingle() { final Index index = index(Single.class, SingleImpl.class, SingleFromInterfaceAndSuperClass.class); - final DotName impl = DotName.createSimple(SingleFromInterfaceAndSuperClass.class.getName()); - final List result = JandexUtil.resolveTypeParameters(impl, SIMPLE, index); - assertThat(result).extracting("name").containsOnly(STRING); + checkRepoArg(index, SingleFromInterfaceAndSuperClass.class, Single.class, String.class); } @Test public void testExtendsAbstractClass() { - final DotName abstractSingle = DotName.createSimple(AbstractSingle.class.getName()); final Index index = index(Single.class, AbstractSingle.class, AbstractSingleImpl.class, ExtendsAbstractSingleImpl.class); - assertThat(JandexUtil.resolveTypeParameters(DotName.createSimple(AbstractSingleImpl.class.getName()), abstractSingle, - index)).extracting("name").containsOnly(INTEGER); - assertThat(JandexUtil.resolveTypeParameters(DotName.createSimple(ExtendsAbstractSingleImpl.class.getName()), - abstractSingle, index)).extracting("name").containsOnly(INTEGER); + checkRepoArg(index, AbstractSingleImpl.class, AbstractSingle.class, Integer.class); + checkRepoArg(index, ExtendsAbstractSingleImpl.class, AbstractSingle.class, Integer.class); + } + + @Test + public void testArrayGenerics() { + final Index index = index(Repo.class, ArrayRepo.class, GenericArrayRepo.class); + checkRepoArg(index, ArrayRepo.class, Repo.class, Integer[].class); + } + + @Test + public void testCompositeGenerics() { + final Index index = index(Repo.class, Repo2.class, CompositeRepo.class, CompositeRepo2.class, + GenericCompositeRepo.class, GenericCompositeRepo2.class); + checkRepoArg(index, CompositeRepo.class, Repo.class, Repo.class.getName() + ""); + checkRepoArg(index, CompositeRepo2.class, Repo2.class, Repo.class.getName() + ""); + } + + @Test + public void testErasedGenerics() { + final Index index = index(Repo.class, BoundedRepo.class, ErasedRepo1.class, MultiBoundedRepo.class, ErasedRepo2.class, + A.class); + checkRepoArg(index, ErasedRepo1.class, Repo.class, A.class); + checkRepoArg(index, ErasedRepo2.class, Repo.class, A.class); } public interface Single { @@ -246,6 +238,60 @@ public static class ExtendsMultipleT1Impl implements MultipleT1 { public static class SingleFromInterfaceAndSuperClass extends SingleImpl implements Single { } + public interface Repo { + } + + public interface Repo2 { + } + + public static class DirectRepo implements Repo { + } + + public static class IndirectRepo extends DirectRepo { + } + + public static class GenericRepo implements Repo { + } + + public static class IndirectGenericRepo extends GenericRepo { + } + + public static class GenericArrayRepo implements Repo { + } + + public static class ArrayRepo extends GenericArrayRepo { + } + + public static class GenericCompositeRepo implements Repo> { + } + + public static class GenericCompositeRepo2 implements Repo2> { + } + + public static class CompositeRepo extends GenericCompositeRepo { + } + + public static class CompositeRepo2 extends GenericCompositeRepo2 { + } + + public static class BoundedRepo implements Repo { + } + + public static class ErasedRepo1 extends BoundedRepo { + } + + public interface A { + } + + public interface B { + } + + public static class MultiBoundedRepo implements Repo { + } + + public static class ErasedRepo2 extends MultiBoundedRepo { + } + private static Index index(Class... classes) { Indexer indexer = new Indexer(); for (Class clazz : classes) { @@ -261,4 +307,32 @@ private static Index index(Class... classes) { return indexer.complete(); } + private void checkRepoArg(Index index, Class baseClass, Class soughtClass, Class expectedArg) { + List args = JandexUtil.resolveTypeParameters(name(baseClass), name(soughtClass), + index); + assertThat(args).extracting("name").containsOnly(name(expectedArg)); + } + + private void checkRepoArg(Index index, Class baseClass, Class soughtClass, Class... expectedArgs) { + List args = JandexUtil.resolveTypeParameters(name(baseClass), name(soughtClass), + index); + Object[] expectedArgNames = new Object[expectedArgs.length]; + for (int i = 0; i < expectedArgs.length; i++) { + expectedArgNames[i] = name(expectedArgs[i]); + } + assertThat(args).extracting("name").containsOnly(expectedArgNames); + } + + private void checkRepoArg(Index index, Class baseClass, Class soughtClass, String expectedArg) { + List args = JandexUtil.resolveTypeParameters(name(baseClass), name(soughtClass), + index); + assertThat(args).hasOnlyOneElementSatisfying(t -> { + assertThat(t.toString()).isEqualTo(expectedArg); + }); + } + + private static DotName name(Class klass) { + return DotName.createSimple(klass.getName()); + } + } diff --git a/core/devmode/src/main/java/io/quarkus/dev/ClassLoaderCompiler.java b/core/devmode/src/main/java/io/quarkus/dev/ClassLoaderCompiler.java index 4a42569d1cdb3..e8896a53e5935 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/ClassLoaderCompiler.java +++ b/core/devmode/src/main/java/io/quarkus/dev/ClassLoaderCompiler.java @@ -122,12 +122,19 @@ public ClassLoaderCompiler(ClassLoader classLoader, } Object classPath = mf.getMainAttributes().get(Attributes.Name.CLASS_PATH); if (classPath != null) { - for (String i : WHITESPACE_PATTERN.split(classPath.toString())) { + for (String classPathEntry : WHITESPACE_PATTERN.split(classPath.toString())) { + final URI cpEntryURI = new URI(classPathEntry); File f; - try { - f = Paths.get(new URI("file", null, "/", null).resolve(new URI(i))).toFile(); - } catch (URISyntaxException e) { - f = new File(file.getParentFile(), i); + // if it's a "file" scheme URI, then use the path as a file system path + // without the need to resolve it + if (cpEntryURI.isAbsolute() && cpEntryURI.getScheme().equals("file")) { + f = new File(cpEntryURI.getPath()); + } else { + try { + f = Paths.get(new URI("file", null, "/", null).resolve(cpEntryURI)).toFile(); + } catch (URISyntaxException e) { + f = new File(file.getParentFile(), classPathEntry); + } } if (f.exists()) { toParse.add(f.getAbsolutePath()); @@ -159,7 +166,9 @@ public ClassLoaderCompiler(ClassLoader classLoader, context.getSourceEncoding(), context.getCompilerOptions(), context.getSourceJavaVersion(), - context.getTargetJvmVersion())); + context.getTargetJvmVersion(), + context.getCompilerPluginArtifacts(), + context.getCompilerPluginsOptions())); }); } } diff --git a/core/devmode/src/main/java/io/quarkus/dev/CompilationProvider.java b/core/devmode/src/main/java/io/quarkus/dev/CompilationProvider.java index 864d01b938b2a..253494593b6e8 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/CompilationProvider.java +++ b/core/devmode/src/main/java/io/quarkus/dev/CompilationProvider.java @@ -32,6 +32,8 @@ class Context { private final List compilerOptions; private final String sourceJavaVersion; private final String targetJvmVersion; + private final List compilePluginArtifacts; + private final List compilerPluginOptions; public Context( String name, @@ -42,7 +44,9 @@ public Context( String sourceEncoding, List compilerOptions, String sourceJavaVersion, - String targetJvmVersion) { + String targetJvmVersion, + List compilePluginArtifacts, + List compilerPluginOptions) { this.name = name; this.classpath = classpath; @@ -53,6 +57,8 @@ public Context( this.compilerOptions = compilerOptions == null ? new ArrayList() : compilerOptions; this.sourceJavaVersion = sourceJavaVersion; this.targetJvmVersion = targetJvmVersion; + this.compilePluginArtifacts = compilePluginArtifacts; + this.compilerPluginOptions = compilerPluginOptions; } public String getName() { @@ -90,5 +96,13 @@ public String getSourceJavaVersion() { public String getTargetJvmVersion() { return targetJvmVersion; } + + public List getCompilePluginArtifacts() { + return compilePluginArtifacts; + } + + public List getCompilerPluginOptions() { + return compilerPluginOptions; + } } } diff --git a/core/devmode/src/main/java/io/quarkus/dev/DevModeContext.java b/core/devmode/src/main/java/io/quarkus/dev/DevModeContext.java index 31f56a5b19ea1..6d4a0e0d2cc05 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/DevModeContext.java +++ b/core/devmode/src/main/java/io/quarkus/dev/DevModeContext.java @@ -5,7 +5,9 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -36,6 +38,9 @@ public class DevModeContext implements Serializable { private String sourceJavaVersion; private String targetJvmVersion; + private List compilerPluginArtifacts; + private List compilerPluginsOptions; + public List getClassPath() { return classPath; } @@ -120,6 +125,22 @@ public void setTargetJvmVersion(String targetJvmVersion) { this.targetJvmVersion = targetJvmVersion; } + public List getCompilerPluginArtifacts() { + return compilerPluginArtifacts; + } + + public void setCompilerPluginArtifacts(List compilerPluginArtifacts) { + this.compilerPluginArtifacts = compilerPluginArtifacts; + } + + public List getCompilerPluginsOptions() { + return compilerPluginsOptions; + } + + public void setCompilerPluginsOptions(List compilerPluginsOptions) { + this.compilerPluginsOptions = compilerPluginsOptions; + } + public File getDevModeRunnerJarFile() { return devModeRunnerJarFile; } @@ -144,7 +165,7 @@ public ModuleInfo( String resourcePath) { this.name = name; this.projectDirectory = projectDirectory; - this.sourcePaths = sourcePaths; + this.sourcePaths = sourcePaths == null ? new HashSet<>() : new HashSet<>(sourcePaths); this.classesPath = classesPath; this.resourcePath = resourcePath; } @@ -158,7 +179,7 @@ public String getProjectDirectory() { } public Set getSourcePaths() { - return sourcePaths; + return Collections.unmodifiableSet(sourcePaths); } public void addSourcePaths(Collection additionalPaths) { @@ -173,5 +194,4 @@ public String getResourcePath() { return resourcePath; } } - } diff --git a/core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java b/core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java index 47e6b773f4b00..04ea9a369c325 100644 --- a/core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java +++ b/core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java @@ -21,6 +21,7 @@ import java.util.concurrent.locks.LockSupport; import java.util.function.Consumer; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.logging.Logger; import io.quarkus.builder.BuildChainBuilder; @@ -32,7 +33,7 @@ import io.quarkus.runner.RuntimeRunner; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.Timing; -import io.smallrye.config.SmallRyeConfigProviderResolver; +import io.quarkus.runtime.configuration.QuarkusConfigFactory; /** * The main entry point for the dev mojo execution @@ -262,7 +263,13 @@ public void stop() { Thread.currentThread().setContextClassLoader(old); } } - SmallRyeConfigProviderResolver.instance().releaseConfig(SmallRyeConfigProviderResolver.instance().getConfig()); + QuarkusConfigFactory.setConfig(null); + final ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + try { + cpr.releaseConfig(cpr.getConfig()); + } catch (IllegalStateException ignored) { + // just means no config was installed, which is fine + } DevModeMain.runner = null; } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java b/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java index c43b06a85e93d..d5025d22624ed 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java @@ -17,7 +17,6 @@ final public class Constants { public static final char DOT = '.'; public static final String EMPTY = ""; public static final String DASH = "-"; - public static final String CORE = "core-"; public static final String ADOC_EXTENSION = ".adoc"; public static final String DIGIT_OR_LOWERCASE = "^[a-z0-9]+$"; @@ -30,7 +29,6 @@ final public class Constants { public static final String DEPLOYMENT = "deployment"; public static final Pattern CLASS_NAME_PATTERN = Pattern.compile("^.+[\\.$](\\w+)$"); - public static final Pattern CONFIG_ROOT_PATTERN = Pattern.compile("^(\\w+)Config(uration)?"); public static final Pattern PKG_PATTERN = Pattern.compile("^io\\.quarkus\\.(\\w+)\\.?(\\w+)?\\.?(\\w+)?"); public static final String INSTANCE_SYM = "__instance"; diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java index 5ab400889ac41..843d0ccdb0308 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java @@ -65,9 +65,10 @@ import org.jboss.jdeparser.JType; import org.jboss.jdeparser.JTypes; +import io.quarkus.annotation.processor.generate_doc.ConfigDocGeneratedOutput; import io.quarkus.annotation.processor.generate_doc.ConfigDocItemScanner; import io.quarkus.annotation.processor.generate_doc.ConfigDocWriter; -import io.quarkus.annotation.processor.generate_doc.ScannedConfigDocsItemHolder; +import io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil; public class ExtensionAnnotationProcessor extends AbstractProcessor { @@ -235,12 +236,12 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } try { - final ScannedConfigDocsItemHolder scannedConfigDocsItemHolder = configDocItemScanner + final Set outputs = configDocItemScanner .scanExtensionsConfigurationItems(javaDocProperties); - - configDocWriter.writeExtensionConfigDocumentation(scannedConfigDocsItemHolder.getAllConfigItemsPerExtension(), - true); // generate extension doc with search engine activate - configDocWriter.writeExtensionConfigDocumentation(scannedConfigDocsItemHolder.getConfigGroupConfigItems(), false); // generate config group docs with search engine deactivated + for (ConfigDocGeneratedOutput output : outputs) { + DocGeneratorUtil.sort(output.getConfigDocItems()); // sort before writing + configDocWriter.writeAllExtensionConfigDocumentation(output); + } } catch (IOException e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate extension doc: " + e); return; diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java index 0db6079bb6a1e..1bf8aa40940f2 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Set; @@ -19,7 +20,9 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import io.quarkus.annotation.processor.Constants; @@ -54,7 +57,7 @@ ScannedConfigDocsItemHolder findInMemoryConfigurationItems() { final int sectionLevel = 2; final List configDocItems = recursivelyFindConfigItems(element, configRootInfo.getName(), configRootInfo.getConfigPhase(), false, sectionLevel); - holder.addToAllConfigItems(configRootInfo.getClazz().getQualifiedName().toString(), configDocItems); + holder.addConfigRootItems(configRootInfo.getClazz().getQualifiedName().toString(), configDocItems); } return holder; @@ -164,14 +167,15 @@ private List recursivelyFindConfigItems(Element element, String p } else { final ConfigDocKey configDocKey = new ConfigDocKey(); configDocKey.setWithinAMap(withinAMap); - boolean optional = false; boolean list = false; + boolean optional = false; if (!typeMirror.getKind().isPrimitive()) { DeclaredType declaredType = (DeclaredType) typeMirror; TypeElement typeElement = (TypeElement) declaredType.asElement(); Name qualifiedName = typeElement.getQualifiedName(); - optional = qualifiedName.toString().startsWith("java.util.Optional"); - list = qualifiedName.contentEquals("java.util.List"); + optional = qualifiedName.toString().startsWith(Optional.class.getName()); + list = qualifiedName.contentEquals(List.class.getName()) + || qualifiedName.contentEquals(Set.class.getName()); List typeArguments = declaredType.getTypeArguments(); if (!typeArguments.isEmpty()) { @@ -183,8 +187,7 @@ private List recursivelyFindConfigItems(Element element, String p if (configGroup != null) { name += String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, configDocMapKey); List groupConfigItems = recordConfigItemsFromConfigGroup(configPhase, name, - configGroup, - configSection, true, sectionLevel); + configGroup, configSection, true, sectionLevel); configDocItems.addAll(groupConfigItems); continue; } else { @@ -195,8 +198,40 @@ private List recursivelyFindConfigItems(Element element, String p } else { // FIXME: this is for Optional and List TypeMirror realTypeMirror = typeArguments.get(0); - type = simpleTypeToString(realTypeMirror); + String typeInString = realTypeMirror.toString(); + + if (optional) { + configGroup = configGroups.get(typeInString); + if (configGroup != null) { + if (configSection == null) { + final JavaDocParser.SectionHolder sectionHolder = javaDocParser.parseConfigSection( + rawJavaDoc, + sectionLevel); + configSection = new ConfigDocSection(); + configSection.setWithinAMap(withinAMap); + configSection.setConfigPhase(configPhase); + configSection.setSectionDetails(sectionHolder.details); + configSection.setSectionDetailsTitle(sectionHolder.title); + configSection.setName(parentName + Constants.DOT + hyphenatedFieldName); + } + configSection.setOptional(true); + List groupConfigItems = recordConfigItemsFromConfigGroup(configPhase, name, + configGroup, configSection, withinAMap, sectionLevel); + configDocItems.addAll(groupConfigItems); + continue; + } else if ((typeInString.startsWith(List.class.getName()) + || typeInString.startsWith(Set.class.getName()) + || realTypeMirror.getKind() == TypeKind.ARRAY)) { + list = true; + DeclaredType declaredRealType = (DeclaredType) typeMirror; + typeArguments = declaredRealType.getTypeArguments(); + if (!typeArguments.isEmpty()) { + realTypeMirror = typeArguments.get(0); + } + } + } + type = simpleTypeToString(realTypeMirror); if (isEnumType(realTypeMirror)) { acceptedValues = extractEnumValues(realTypeMirror); } @@ -213,10 +248,11 @@ private List recursivelyFindConfigItems(Element element, String p configDocKey.setKey(name); configDocKey.setType(type); + configDocKey.setList(list); + configDocKey.setOptional(optional); + configDocKey.setWithinAConfigGroup(sectionLevel > 2); configDocKey.setConfigPhase(configPhase); configDocKey.setDefaultValue(defaultValue); - configDocKey.setOptional(optional); - configDocKey.setList(list); configDocKey.setDocMapKey(configDocMapKey); configDocKey.setConfigDoc(configDescription); configDocKey.setAcceptedValues(acceptedValues); @@ -232,19 +268,16 @@ private List recursivelyFindConfigItems(Element element, String p private List recordConfigItemsFromConfigGroup(ConfigPhase configPhase, String name, Element configGroup, ConfigDocSection configSection, boolean withinAMap, int sectionLevel) { - final List groupConfigItems; final List configDocItems = new ArrayList<>(); - - if (configSection != null) { + final List groupConfigItems = recursivelyFindConfigItems(configGroup, name, configPhase, withinAMap, + sectionLevel + 1); + if (configSection == null) { + configDocItems.addAll(groupConfigItems); + } else { final ConfigDocItem configDocItem = new ConfigDocItem(); configDocItem.setConfigDocSection(configSection); configDocItems.add(configDocItem); - groupConfigItems = recursivelyFindConfigItems(configGroup, name, configPhase, withinAMap, - sectionLevel + 1); configSection.addConfigDocItems(groupConfigItems); - } else { - groupConfigItems = recursivelyFindConfigItems(configGroup, name, configPhase, withinAMap, sectionLevel); - configDocItems.addAll(groupConfigItems); } String configGroupName = configGroup.asType().toString(); @@ -259,12 +292,25 @@ private List recordConfigItemsFromConfigGroup(ConfigPhase configP } private String simpleTypeToString(TypeMirror typeMirror) { + if (typeMirror.getKind().isPrimitive()) { return typeMirror.toString(); + } else if (typeMirror.getKind() == TypeKind.ARRAY) { + return simpleTypeToString(((ArrayType) typeMirror).getComponentType()); } final String knownGenericType = getKnownGenericType((DeclaredType) typeMirror); - return knownGenericType != null ? knownGenericType : typeMirror.toString(); + + if (knownGenericType != null) { + return knownGenericType; + } + + List typeArguments = ((DeclaredType) typeMirror).getTypeArguments(); + if (!typeArguments.isEmpty()) { + return simpleTypeToString(typeArguments.get(0)); + } + + return typeMirror.toString(); } private List extractEnumValues(TypeMirror realTypeMirror) { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocGeneratedOutput.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocGeneratedOutput.java new file mode 100644 index 0000000000000..95715a245e897 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocGeneratedOutput.java @@ -0,0 +1,70 @@ +package io.quarkus.annotation.processor.generate_doc; + +import java.util.List; +import java.util.Objects; + +import io.quarkus.annotation.processor.Constants; + +public class ConfigDocGeneratedOutput { + private final String fileName; + private final boolean searchable; + private final boolean hasAnchorPrefix; + private final List configDocItems; + + public ConfigDocGeneratedOutput(String fileName, boolean searchable, List configDocItems, + boolean hasAnchorPrefix) { + this.fileName = fileName; + this.searchable = searchable; + this.configDocItems = configDocItems; + this.hasAnchorPrefix = hasAnchorPrefix; + } + + public String getFileName() { + return fileName; + } + + public boolean isSearchable() { + return searchable; + } + + public List getConfigDocItems() { + return configDocItems; + } + + public String getAnchorPrefix() { + if (!hasAnchorPrefix) { + return Constants.EMPTY; + } + + String anchorPrefix = fileName; + if (fileName.endsWith(Constants.ADOC_EXTENSION)) { + anchorPrefix = anchorPrefix.substring(0, anchorPrefix.length() - 5); + } + + return anchorPrefix + "_"; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ConfigDocGeneratedOutput that = (ConfigDocGeneratedOutput) o; + return Objects.equals(fileName, that.fileName); + } + + @Override + public int hashCode() { + return Objects.hash(fileName); + } + + @Override + public String toString() { + return "ConfigItemsOutput{" + + "fileName='" + fileName + '\'' + + ", searchable=" + searchable + + ", configDocItems=" + configDocItems + + '}'; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItem.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItem.java index a821304d39fa2..3b675205ef8b8 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItem.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItem.java @@ -89,6 +89,17 @@ public boolean isWithinAMap() { return false; } + @JsonIgnore + public boolean isWithinAConfigGroup() { + if (isConfigSection()) { + return true; + } else if (isConfigKey() && configDocKey.isWithinAConfigGroup()) { + return true; + } + + return false; + } + /** * TODO determine section ordering * diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemScanner.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemScanner.java index 965880e9d740c..6eed212fa5388 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemScanner.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemScanner.java @@ -1,8 +1,9 @@ package io.quarkus.annotation.processor.generate_doc; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigGroupDocFileName; +import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigRootDocFileName; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeExtensionDocFileName; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.hyphenate; +import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.deriveConfigRootName; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -69,6 +70,7 @@ public void addConfigRoot(final PackageElement pkg, TypeElement clazz) { } ConfigPhase configPhase = ConfigPhase.BUILD_TIME; + final String extensionName = pkgMatcher.group(1); for (AnnotationMirror annotationMirror : clazz.getAnnotationMirrors()) { String annotationName = annotationMirror.getAnnotationType().toString(); @@ -87,13 +89,12 @@ public void addConfigRoot(final PackageElement pkg, TypeElement clazz) { } if (name.isEmpty()) { - final Matcher nameMatcher = Constants.CONFIG_ROOT_PATTERN.matcher(clazz.getSimpleName()); - if (nameMatcher.find()) { - name = Constants.QUARKUS + Constants.DOT + hyphenate(nameMatcher.group(1)); - } + name = deriveConfigRootName(clazz.getSimpleName().toString(), configPhase); + } else if (name.endsWith(Constants.DOT + Constants.PARENT)) { + // take into account the root case which would contain characters that can't be used to create the final file + name = name.replace(Constants.DOT + Constants.PARENT, ""); } - final String extensionName = pkgMatcher.group(1); ConfigRootInfo configRootInfo = new ConfigRootInfo(name, clazz, extensionName, configPhase); configRoots.add(configRootInfo); break; @@ -101,15 +102,11 @@ public void addConfigRoot(final PackageElement pkg, TypeElement clazz) { } } - /** - * Return a data structure which contains two maps of config items. - * 1. A map of all extensions config items accessible via - * {@link ScannedConfigDocsItemHolder#getAllConfigItemsPerExtension()} - * 2. a map of all config groups config items accessible via {@link ScannedConfigDocsItemHolder#getConfigGroupConfigItems()} - */ - public ScannedConfigDocsItemHolder scanExtensionsConfigurationItems(Properties javaDocProperties) + public Set scanExtensionsConfigurationItems(Properties javaDocProperties) throws IOException { + Set configDocGeneratedOutputs = new HashSet<>(); + final ConfigDoItemFinder configDoItemFinder = new ConfigDoItemFinder(configRoots, configGroups, javaDocProperties); final ScannedConfigDocsItemHolder inMemoryScannedItemsHolder = configDoItemFinder.findInMemoryConfigurationItems(); @@ -125,11 +122,16 @@ public ScannedConfigDocsItemHolder scanExtensionsConfigurationItems(Properties j configurationRootsParExtensionFileName); } - Map> allConfigItemsPerExtension = computeAllExtensionConfigItems(inMemoryScannedItemsHolder, + Set allConfigItemsPerExtension = generateAllConfigItemsOutputs(inMemoryScannedItemsHolder, allExtensionGeneratedDocs, configurationRootsParExtensionFileName); - Map> configGroupConfigItems = computeConfigGroupFilesNames(inMemoryScannedItemsHolder); + Set configGroupConfigItems = generateAllConfigGroupOutputs(inMemoryScannedItemsHolder); + Set configRootConfigItems = generateAllConfigRootOutputs(inMemoryScannedItemsHolder); + + configDocGeneratedOutputs.addAll(configGroupConfigItems); + configDocGeneratedOutputs.addAll(allConfigItemsPerExtension); + configDocGeneratedOutputs.addAll(configRootConfigItems); - return new ScannedConfigDocsItemHolder(allConfigItemsPerExtension, configGroupConfigItems); + return configDocGeneratedOutputs; } private void createOutputFolder() throws IOException { @@ -199,7 +201,7 @@ private void updateConfigurationRootsList(Properties configurationRootsParExtens private void updateScannedExtensionArtifactFiles(ScannedConfigDocsItemHolder inMemoryScannedItemsHolder, Properties allExtensionGeneratedDocs, Properties configurationRootsParExtensionFileName) throws IOException { - for (Map.Entry> entry : inMemoryScannedItemsHolder.getAllConfigItemsPerExtension() + for (Map.Entry> entry : inMemoryScannedItemsHolder.getConfigRootConfigItems() .entrySet()) { String serializableConfigRootDoc = OBJECT_MAPPER.writeValueAsString(entry.getValue()); allExtensionGeneratedDocs.put(entry.getKey(), serializableConfigRootDoc); @@ -223,13 +225,10 @@ private void updateScannedExtensionArtifactFiles(ScannedConfigDocsItemHolder inM } } - /** - * returns a Map of with extension generated file name and the list of its associated config items. - */ - private Map> computeAllExtensionConfigItems( + private Set generateAllConfigItemsOutputs( ScannedConfigDocsItemHolder inMemoryScannedItemsHolder, Properties allExtensionGeneratedDocs, Properties configurationRootsParExtensionFileName) throws IOException { - Map> configItemsParExtensionFileNames = new HashMap<>(); + Set outputs = new HashSet<>(); Set extensionFileNamesToGenerate = processorClassMembers .stream() @@ -244,35 +243,59 @@ private Map> computeAllExtensionConfigItems( } String[] extensionConfigRoots = extensionConfigRootsProperty.split(EXTENSION_LIST_SEPARATOR); + List extensionConfigItems = new ArrayList<>(); + for (String configRoot : extensionConfigRoots) { - List configDocItems = inMemoryScannedItemsHolder.getAllConfigItemsPerExtension().get(configRoot); + List configDocItems = inMemoryScannedItemsHolder.getConfigRootConfigItems().get(configRoot); if (configDocItems == null) { String serializedContent = allExtensionGeneratedDocs.getProperty(configRoot); configDocItems = OBJECT_MAPPER.readValue(serializedContent, new TypeReference>() { }); } - final List existingConfigDocItems = configItemsParExtensionFileNames - .computeIfAbsent(extensionFileName, (key) -> new ArrayList<>()); - DocGeneratorUtil.appendConfigItemsIntoExistingOnes(existingConfigDocItems, configDocItems); + DocGeneratorUtil.appendConfigItemsIntoExistingOnes(extensionConfigItems, configDocItems); + } + + outputs.add(new ConfigDocGeneratedOutput(extensionFileName, true, extensionConfigItems, true)); + + List generalConfigItems = extensionConfigItems + .stream() + .filter(ConfigDocItem::isWithinAConfigGroup) + .collect(Collectors.toList()); + + if (!generalConfigItems.isEmpty()) { + String fileName = extensionFileName.replaceAll("\\.adoc$", "-general-config-items.adoc"); + outputs.add(new ConfigDocGeneratedOutput(fileName, false, generalConfigItems, true)); } + } - return configItemsParExtensionFileNames; + return outputs; } - /** - * returns a Map of with config group generated file name and the list of its associated config items. - */ - private Map> computeConfigGroupFilesNames( + private Set generateAllConfigGroupOutputs( ScannedConfigDocsItemHolder inMemoryScannedItemsHolder) { - Map> configItemsParConfigGroupFileNames = new HashMap<>(); - for (Map.Entry> entry : inMemoryScannedItemsHolder.getConfigGroupConfigItems().entrySet()) { - String extensionDocFileName = computeConfigGroupDocFileName(entry.getKey()); - configItemsParConfigGroupFileNames.put(extensionDocFileName, entry.getValue()); + + return inMemoryScannedItemsHolder + .getConfigGroupConfigItems() + .entrySet() + .stream() + .map(entry -> new ConfigDocGeneratedOutput(computeConfigGroupDocFileName(entry.getKey()), false, + entry.getValue(), true)) + .collect(Collectors.toSet()); + } + + private Set generateAllConfigRootOutputs(ScannedConfigDocsItemHolder inMemoryScannedItemsHolder) { + Map> configRootConfigItems = inMemoryScannedItemsHolder.getConfigRootConfigItems(); + Set outputs = new HashSet<>(); + for (ConfigRootInfo configRootInfo : configRoots) { + String clazz = configRootInfo.getClazz().getQualifiedName().toString(); + List configDocItems = configRootConfigItems.get(clazz); + String fileName = computeConfigRootDocFileName(clazz, configRootInfo.getName()); + outputs.add(new ConfigDocGeneratedOutput(fileName, false, configDocItems, true)); } - return configItemsParConfigGroupFileNames; + return outputs; } /** diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java index 72525c41f75e0..9b2015a7e291e 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java @@ -21,6 +21,7 @@ final public class ConfigDocKey implements ConfigDocElement, Comparable> extensionsConfigurations, - boolean withSearchActivated) - throws IOException { - for (Map.Entry> entry : extensionsConfigurations.entrySet()) { - final List configDocItems = entry.getValue(); - final String fileName = entry.getKey(); - - sort(configDocItems); - String anchorPrefix = fileName; - if (fileName.endsWith(Constants.ADOC_EXTENSION)) { - anchorPrefix = anchorPrefix.substring(0, anchorPrefix.length() - 5); - } - - generateDocumentation(Constants.GENERATED_DOCS_PATH.resolve(fileName), anchorPrefix + "_", withSearchActivated, - configDocItems); - } - } - /** * Write all extension configuration in AsciiDoc format in `{root}/target/asciidoc/generated/config/` directory */ - public void writeAllExtensionConfigDocumentation(List allItems) + public void writeAllExtensionConfigDocumentation(ConfigDocGeneratedOutput output) throws IOException { - generateDocumentation(Constants.GENERATED_DOCS_PATH.resolve("all-config.adoc"), "", true, allItems); - } - - /** - * Sort docs keys. The sorted list will contain the properties in the following order - * - 1. Map config items as last elements of the generated docs. - * - 2. Build time properties will come first. - * - 3. Otherwise respect source code declaration order. - * - 4. Elements within a configuration section will appear at the end of the generated doc while preserving described in - * 1-4. - */ - public static void sort(List configDocItems) { - Collections.sort(configDocItems); - for (ConfigDocItem configDocItem : configDocItems) { - if (configDocItem.isConfigSection()) { - sort(configDocItem.getConfigDocSection().getConfigDocItems()); - } - } + generateDocumentation(Constants.GENERATED_DOCS_PATH.resolve(output.getFileName()), output.getAnchorPrefix(), + output.isSearchable(), output.getConfigDocItems()); } /** diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigPhase.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigPhase.java index cd9708af3e6dd..55e72faab481f 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigPhase.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigPhase.java @@ -5,9 +5,10 @@ import io.quarkus.annotation.processor.Constants; public enum ConfigPhase implements Comparable { - RUN_TIME("The configuration is overridable at runtime", Constants.CONFIG_PHASE_RUNTIME_ILLUSTRATION), - BUILD_TIME("The configuration is not overridable at runtime", Constants.CONFIG_PHASE_BUILD_TIME_ILLUSTRATION), - BUILD_AND_RUN_TIME_FIXED("The configuration is not overridable at runtime", Constants.CONFIG_PHASE_BUILD_TIME_ILLUSTRATION); + RUN_TIME("The configuration is overridable at runtime", Constants.CONFIG_PHASE_RUNTIME_ILLUSTRATION, "RunTime"), + BUILD_TIME("The configuration is not overridable at runtime", Constants.CONFIG_PHASE_BUILD_TIME_ILLUSTRATION, "BuildTime"), + BUILD_AND_RUN_TIME_FIXED("The configuration is not overridable at runtime", Constants.CONFIG_PHASE_BUILD_TIME_ILLUSTRATION, + "BuildTime"); static final Comparator COMPARATOR = new Comparator() { /** @@ -52,10 +53,12 @@ public int compare(ConfigPhase firstPhase, ConfigPhase secondPhase) { private String description; private String illustration; + private String configSuffix; - ConfigPhase(String description, String illustration) { + ConfigPhase(String description, String illustration, String configSuffix) { this.description = description; this.illustration = illustration; + this.configSuffix = configSuffix; } @Override @@ -63,10 +66,15 @@ public String toString() { return "ConfigPhase{" + "description='" + description + '\'' + ", illustration='" + illustration + '\'' + + ", configSuffix='" + configSuffix + '\'' + '}'; } public String getIllustration() { return illustration; } + + public String getConfigSuffix() { + return configSuffix; + } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtil.java index 8cf060413e738..91cd934472d90 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtil.java @@ -1,6 +1,7 @@ package io.quarkus.annotation.processor.generate_doc; import java.time.Duration; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -17,7 +18,10 @@ import io.quarkus.annotation.processor.Constants; public class DocGeneratorUtil { - private static String CONFIG_GROUP_PREFIX = "config-group-"; + private static final String CORE = "core"; + private static final String CONFIG = "Config"; + private static final String CONFIGURATION = "Configuration"; + private static String CONFIG_GROUP_DOC_PREFIX = "config-group-"; static final String VERTX_JAVA_DOC_SITE = "https://vertx.io/docs/apidocs/"; static final String OFFICIAL_JAVA_DOC_BASE_LINK = "https://docs.oracle.com/javase/8/docs/api/"; static final String AGROAL_API_JAVA_DOC_SITE = "https://jar-download.com/javaDoc/io.agroal/agroal-api/1.5/index.html?"; @@ -98,8 +102,12 @@ static String getJavaDocSiteLink(String type) { } private static String getJavaDocLinkForType(String type) { - int indexOfFirstUpperCase = 0; + int beginOfWrappedTypeIndex = type.indexOf("<"); + if (beginOfWrappedTypeIndex != -1) { + type = type.substring(0, beginOfWrappedTypeIndex); + } + int indexOfFirstUpperCase = 0; for (int index = 0; index < type.length(); index++) { char charAt = type.charAt(index); if (charAt >= 'A' && charAt <= 'Z') { @@ -211,12 +219,12 @@ public String next() { }; } - static String join(String delim, Iterator it) { + static String join(Iterator it) { final StringBuilder b = new StringBuilder(); if (it.hasNext()) { b.append(it.next()); while (it.hasNext()) { - b.append(delim); + b.append("-"); b.append(it.next()); } } @@ -224,7 +232,7 @@ static String join(String delim, Iterator it) { } static String hyphenate(String orig) { - return join("-", lowerCase(camelHumpsIterator(orig))); + return join(lowerCase(camelHumpsIterator(orig))); } static String hyphenateEnumValue(String orig) { @@ -272,10 +280,46 @@ public static String computeExtensionDocFileName(String configRoot) { extensionNameBuilder.append(Constants.DASH); if (Constants.DEPLOYMENT.equals(extensionName) || Constants.RUNTIME.equals(extensionName)) { - final String configClass = configRoot.substring(configRoot.lastIndexOf(Constants.DOT) + 1); - extensionName = hyphenate(configClass); - extensionNameBuilder.append(Constants.CORE); + extensionNameBuilder.append(CORE); + } else if (subgroup != null && !Constants.DEPLOYMENT.equals(subgroup) + && !Constants.RUNTIME.equals(subgroup) && !Constants.COMMON.equals(subgroup) + && subgroup.matches(Constants.DIGIT_OR_LOWERCASE)) { + extensionNameBuilder.append(extensionName); + extensionNameBuilder.append(Constants.DASH); + extensionNameBuilder.append(subgroup); + + final String qualifier = matcher.group(3); + if (qualifier != null && !Constants.DEPLOYMENT.equals(qualifier) + && !Constants.RUNTIME.equals(qualifier) && !Constants.COMMON.equals(qualifier) + && qualifier.matches(Constants.DIGIT_OR_LOWERCASE)) { + extensionNameBuilder.append(Constants.DASH); + extensionNameBuilder.append(qualifier); + } + } else { extensionNameBuilder.append(extensionName); + } + } + + extensionNameBuilder.append(Constants.ADOC_EXTENSION); + return extensionNameBuilder.toString(); + } + + /** + * Guess extension name from given configuration root class name + */ + public static String computeExtensionGeneralConfigDocFileName(String configRoot) { + StringBuilder extensionNameBuilder = new StringBuilder(); + final Matcher matcher = Constants.PKG_PATTERN.matcher(configRoot); + if (!matcher.find()) { + extensionNameBuilder.append(configRoot); + } else { + String extensionName = matcher.group(1); + final String subgroup = matcher.group(2); + extensionNameBuilder.append(Constants.QUARKUS); + extensionNameBuilder.append(Constants.DASH); + + if (Constants.DEPLOYMENT.equals(extensionName) || Constants.RUNTIME.equals(extensionName)) { + extensionNameBuilder.append(CORE); } else if (subgroup != null && !Constants.DEPLOYMENT.equals(subgroup) && !Constants.RUNTIME.equals(subgroup) && !Constants.COMMON.equals(subgroup) && subgroup.matches(Constants.DIGIT_OR_LOWERCASE)) { @@ -302,17 +346,49 @@ public static String computeExtensionDocFileName(String configRoot) { /** * Guess config group file name from given configuration group class name */ - public static String computeConfigGroupDocFileName(String configGroup) { - final Matcher matcher = Constants.PKG_PATTERN.matcher(configGroup); + public static String computeConfigGroupDocFileName(String configGroupClassName) { + final String sanitizedClassName; + final Matcher matcher = Constants.PKG_PATTERN.matcher(configGroupClassName); + if (!matcher.find()) { - return CONFIG_GROUP_PREFIX + hyphenate(configGroup) + Constants.ADOC_EXTENSION; + sanitizedClassName = CONFIG_GROUP_DOC_PREFIX + Constants.DASH + hyphenate(configGroupClassName); + } else { + String replacement = Constants.DASH + CONFIG_GROUP_DOC_PREFIX + Constants.DASH; + sanitizedClassName = configGroupClassName + .replaceFirst("io.", "") + .replaceFirst("\\.runtime\\.", replacement) + .replaceFirst("\\.deployment\\.", replacement); } - String replacement = Constants.DASH + CONFIG_GROUP_PREFIX; - String sanitizedClassName = configGroup - .replaceFirst("io.", "") - .replaceFirst("\\.runtime\\.", replacement) - .replaceFirst("\\.deployment\\.", replacement); + return hyphenate(sanitizedClassName) + .replaceAll("[\\.-]+", Constants.DASH) + + Constants.ADOC_EXTENSION; + } + + /** + * Guess config root file name from given configuration root class name. + */ + public static String computeConfigRootDocFileName(String configRootClassName, String rootName) { + String sanitizedClassName; + final Matcher matcher = Constants.PKG_PATTERN.matcher(configRootClassName); + + if (!matcher.find()) { + sanitizedClassName = rootName + Constants.DASH + hyphenate(configRootClassName); + } else { + String deployment = Constants.DOT + Constants.DEPLOYMENT + Constants.DOT; + String runtime = Constants.DOT + Constants.RUNTIME + Constants.DOT; + + if (configRootClassName.contains(deployment)) { + sanitizedClassName = configRootClassName + .substring(configRootClassName.indexOf(deployment) + deployment.length()); + } else if (configRootClassName.contains(runtime)) { + sanitizedClassName = configRootClassName.substring(configRootClassName.indexOf(runtime) + runtime.length()); + } else { + sanitizedClassName = configRootClassName.replaceFirst("io.quarkus.", ""); + } + + sanitizedClassName = rootName + Constants.DASH + sanitizedClassName; + } return hyphenate(sanitizedClassName) .replaceAll("[\\.-]+", Constants.DASH) @@ -380,4 +456,40 @@ private static String typeSimpleName(TypeMirror typeMirror) { String type = ((DeclaredType) typeMirror).asElement().toString(); return type.substring(1 + type.lastIndexOf(Constants.DOT)); } + + static String deriveConfigRootName(String simpleClassName, ConfigPhase configPhase) { + String simpleNameInLowerCase = simpleClassName.toLowerCase(); + int length = simpleNameInLowerCase.length(); + + if (simpleNameInLowerCase.endsWith(CONFIG.toLowerCase())) { + String sanitized = simpleClassName.substring(0, length - CONFIG.length()); + return deriveConfigRootName(sanitized, configPhase); + } else if (simpleNameInLowerCase.endsWith(CONFIGURATION.toLowerCase())) { + String sanitized = simpleClassName.substring(0, length - CONFIGURATION.length()); + return deriveConfigRootName(sanitized, configPhase); + } else if (simpleNameInLowerCase.endsWith(configPhase.getConfigSuffix().toLowerCase())) { + String sanitized = simpleClassName.substring(0, length - configPhase.getConfigSuffix().length()); + return deriveConfigRootName(sanitized, configPhase); + } + + return Constants.QUARKUS + Constants.DOT + hyphenate(simpleClassName); + } + + /** + * Sort docs keys. The sorted list will contain the properties in the following order + * - 1. Map config items as last elements of the generated docs. + * - 2. Build time properties will come first. + * - 3. Otherwise respect source code declaration order. + * - 4. Elements within a configuration section will appear at the end of the generated doc while preserving described in + * 1-4. + */ + public static void sort(List configDocItems) { + Collections.sort(configDocItems); + for (ConfigDocItem configDocItem : configDocItems) { + if (configDocItem.isConfigSection()) { + sort(configDocItem.getConfigDocSection().getConfigDocItems()); + } + } + } + } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ScannedConfigDocsItemHolder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ScannedConfigDocsItemHolder.java index 3a9e309706a9d..803c1dc8a5f49 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ScannedConfigDocsItemHolder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ScannedConfigDocsItemHolder.java @@ -4,37 +4,47 @@ import java.util.List; import java.util.Map; -final public class ScannedConfigDocsItemHolder { - private final Map> allConfigItemsPerExtension; +final class ScannedConfigDocsItemHolder { + private final Map> generalConfigItems; + private final Map> configRootConfigItems; private final Map> configGroupConfigItems; public ScannedConfigDocsItemHolder() { - this(new HashMap<>(), new HashMap<>()); + this(new HashMap<>(), new HashMap<>(), new HashMap<>()); } - public ScannedConfigDocsItemHolder(Map> allConfigItemsPerExtension, - Map> configGroupConfigItems) { - this.allConfigItemsPerExtension = allConfigItemsPerExtension; + public ScannedConfigDocsItemHolder(Map> configRootConfigItems, + Map> configGroupConfigItems, Map> generalConfigItems) { + this.configRootConfigItems = configRootConfigItems; this.configGroupConfigItems = configGroupConfigItems; - } - - public Map> getAllConfigItemsPerExtension() { - return allConfigItemsPerExtension; + this.generalConfigItems = generalConfigItems; } public Map> getConfigGroupConfigItems() { return configGroupConfigItems; } - public void addToAllConfigItems(String configRootName, List configDocItems) { - allConfigItemsPerExtension.put(configRootName, configDocItems); + public Map> getConfigRootConfigItems() { + return configRootConfigItems; + } + + public Map> getGeneralConfigItems() { + return generalConfigItems; } public void addConfigGroupItems(String configGroupName, List configDocItems) { configGroupConfigItems.put(configGroupName, configDocItems); } + public void addConfigRootItems(String configRoot, List configDocItems) { + configRootConfigItems.put(configRoot, configDocItems); + } + + public void addGeneralConfigItems(String configRoot, List configDocItems) { + configRootConfigItems.put(configRoot, configDocItems); + } + public boolean isEmpty() { - return allConfigItemsPerExtension.isEmpty(); + return configRootConfigItems.isEmpty(); } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java index d75d66017d297..d270d7c349cd5 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java @@ -11,7 +11,8 @@ final class SummaryTableDocFormatter implements DocFormatter { public static final String SEARCHABLE_TABLE_CLASS = ".searchable"; // a css class indicating if a table is searchable public static final String CONFIGURATION_TABLE_CLASS = ".configuration-reference"; private static final String TABLE_ROW_FORMAT = "\n\na|%s [[%s]]`link:#%s[%s]`\n\n[.description]\n--\n%s\n--|%s %s\n|%s\n"; - private static final String TABLE_SECTION_ROW_FORMAT = "\n\nh|[[%s]]link:#%s[%s]\nh|Type\nh|Default"; + private static final String SECTION_TITLE = "[[%s]]link:#%s[%s]"; + private static final String TABLE_SECTION_ROW_FORMAT = "\n\nh|%s\n%s\nh|Type\nh|Default"; private static final String TABLE_HEADER_FORMAT = "[.configuration-legend]%s\n[%s, cols=\"80,.^10,.^10\"]\n|==="; private String anchorPrefix = ""; @@ -32,7 +33,9 @@ public void format(Writer writer, String initialAnchorPrefix, boolean activateSe // make sure that section-less configs get a legend if (configDocItems.isEmpty() || configDocItems.get(0).isConfigKey()) { String anchor = anchorPrefix + getAnchor("configuration"); - writer.append(String.format(TABLE_SECTION_ROW_FORMAT, anchor, anchor, "Configuration property")); + writer.append(String.format(TABLE_SECTION_ROW_FORMAT, + String.format(SECTION_TITLE, anchor, anchor, "Configuration property"), + Constants.EMPTY)); } for (ConfigDocItem configDocItem : configDocItems) { @@ -88,8 +91,10 @@ public void format(Writer writer, ConfigDocKey configDocKey) throws IOException @Override public void format(Writer writer, ConfigDocSection configDocSection) throws IOException { String anchor = anchorPrefix + getAnchor(configDocSection.getName()); - final String sectionRow = String.format(TABLE_SECTION_ROW_FORMAT, anchor, anchor, - configDocSection.getSectionDetailsTitle()); + String sectionTitle = String.format(SECTION_TITLE, anchor, anchor, configDocSection.getSectionDetailsTitle()); + final String sectionRow = String.format(TABLE_SECTION_ROW_FORMAT, sectionTitle, + configDocSection.isOptional() ? "This configuration section is optional" : Constants.EMPTY); + writer.append(sectionRow); for (ConfigDocItem configDocItem : configDocSection.getConfigDocItems()) { diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtilTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtilTest.java index 3ff77a75b2fb4..d27fb8fefc25e 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtilTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/DocGeneratorUtilTest.java @@ -5,7 +5,9 @@ import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.VERTX_JAVA_DOC_SITE; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.appendConfigItemsIntoExistingOnes; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigGroupDocFileName; +import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeConfigRootDocFileName; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.computeExtensionDocFileName; +import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.deriveConfigRootName; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.getJavaDocSiteLink; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -93,6 +95,12 @@ public void shouldReturnALinkToOfficialJavaDocIfIsJavaOfficialType() { value = getJavaDocSiteLink(Map.Entry.class.getName()); assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/Map.Entry.html", value); + + value = getJavaDocSiteLink(List.class.getName()); + assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/List.html", value); + + value = getJavaDocSiteLink("java.util.List"); + assertEquals(OFFICIAL_JAVA_DOC_BASE_LINK + "java/util/List.html", value); } @Test @@ -131,19 +139,19 @@ public void shouldReturnConfigRootNameWhenComputingExtensionName() { } @Test - public void shouldAddCoreInComputedExtensionName() { + public void shouldUseCoreForConfigRootsCoreModuleWhenComputingExtensionName() { String configRoot = "io.quarkus.runtime.RuntimeConfig"; - String expected = "quarkus-core-runtime-config.adoc"; + String expected = "quarkus-core.adoc"; String fileName = computeExtensionDocFileName(configRoot); assertEquals(expected, fileName); configRoot = "io.quarkus.deployment.BuildTimeConfig"; - expected = "quarkus-core-build-time-config.adoc"; + expected = "quarkus-core.adoc"; fileName = computeExtensionDocFileName(configRoot); assertEquals(expected, fileName); configRoot = "io.quarkus.deployment.path.BuildTimeConfig"; - expected = "quarkus-core-build-time-config.adoc"; + expected = "quarkus-core.adoc"; fileName = computeExtensionDocFileName(configRoot); assertEquals(expected, fileName); } @@ -199,6 +207,34 @@ public void shouldUseHyphenatedClassNameWithoutRuntimeOrDeploymentNamespaceWhenC assertEquals(expected, fileName); } + @Test + public void shouldUseHyphenatedClassNameWithEverythingBeforeRuntimeOrDeploymentNamespaceReplacedByConfigRootNameWhenComputingConfigRootFileName() { + String configRoot = "ClassName"; + String expected = "root-name-class-name.adoc"; + String fileName = computeConfigRootDocFileName(configRoot, "root-name"); + assertEquals(expected, fileName); + + configRoot = "io.quarkus.agroal.runtime.ClassName"; + expected = "quarkus-datasource-class-name.adoc"; + fileName = computeConfigRootDocFileName(configRoot, "quarkus-datasource"); + assertEquals(expected, fileName); + + configRoot = "io.quarkus.keycloak.deployment.RealmConfig"; + expected = "quarkus-keycloak-realm-config.adoc"; + fileName = computeConfigRootDocFileName(configRoot, "quarkus-keycloak"); + assertEquals(expected, fileName); + + configRoot = "io.quarkus.extension.deployment.BuildTimeConfig"; + expected = "quarkus-root-10-build-time-config.adoc"; + fileName = computeConfigRootDocFileName(configRoot, "quarkus-root-10"); + assertEquals(expected, fileName); + + configRoot = "io.quarkus.extension.deployment.name.BuildTimeConfig"; + expected = "quarkus-config-root-name-build-time-config.adoc"; + fileName = computeConfigRootDocFileName(configRoot, "quarkus-config-root"); + assertEquals(expected, fileName); + } + @Test public void shouldPreserveExistingConfigItemsWhenAppendAnEmptyConfigItems() { List existingConfigItems = Arrays.asList(new ConfigDocItem(), new ConfigDocItem()); @@ -279,4 +315,43 @@ public void shouldDeepAppendConfigSectionConfigItemsIntoExistingConfigItemsOfCon assertEquals(deepConfigKey, deepSection.getConfigDocItems().get(0)); assertEquals(configItem, deepSection.getConfigDocItems().get(1)); } + + @Test + public void derivingConfigRootNameTestCase() { + // should hyphenate class name + String simpleClassName = "RootName"; + String actual = deriveConfigRootName(simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + // should hyphenate class name after removing Config(uration) suffix + simpleClassName = "RootNameConfig"; + actual = deriveConfigRootName(simpleClassName, ConfigPhase.BUILD_TIME); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameConfiguration"; + actual = deriveConfigRootName(simpleClassName, ConfigPhase.BUILD_AND_RUN_TIME_FIXED); + assertEquals("quarkus.root-name", actual); + + // should hyphenate class name after removing RunTimeConfig(uration) suffix + simpleClassName = "RootNameRunTimeConfig"; + actual = deriveConfigRootName(simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameRuntimeConfig"; + actual = deriveConfigRootName(simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameRunTimeConfiguration"; + actual = deriveConfigRootName(simpleClassName, ConfigPhase.RUN_TIME); + assertEquals("quarkus.root-name", actual); + + // should hyphenate class name after removing BuildTimeConfig(uration) suffix + simpleClassName = "RootNameBuildTimeConfig"; + actual = deriveConfigRootName(simpleClassName, ConfigPhase.BUILD_AND_RUN_TIME_FIXED); + assertEquals("quarkus.root-name", actual); + + simpleClassName = "RootNameBuildTimeConfiguration"; + actual = deriveConfigRootName(simpleClassName, ConfigPhase.BUILD_TIME); + assertEquals("quarkus.root-name", actual); + } } diff --git a/core/runtime/pom.xml b/core/runtime/pom.xml index 5dd0cff829c7f..8337866bb83ff 100644 --- a/core/runtime/pom.xml +++ b/core/runtime/pom.xml @@ -28,7 +28,7 @@ jakarta.inject-api - io.smallrye + io.smallrye.config smallrye-config diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Application.java b/core/runtime/src/main/java/io/quarkus/runtime/Application.java index 8cc773f761324..b51146fc4dd0f 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/Application.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/Application.java @@ -4,7 +4,6 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.LockSupport; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.graalvm.nativeimage.ImageInfo; import org.wildfly.common.Assert; import org.wildfly.common.lock.Locks; @@ -41,12 +40,6 @@ public abstract class Application { private volatile boolean shutdownRequested; private static volatile Application currentApplication; - /** - * The generated config code will install a new resolver, we save the original one here and make sure - * to restore it on shutdown. - */ - private final static ConfigProviderResolver originalResolver = ConfigProviderResolver.instance(); - /** * Construct a new instance. */ @@ -161,7 +154,6 @@ public final void stop() { doStop(); } finally { currentApplication = null; - ConfigProviderResolver.setInstance(originalResolver); stateLock.lock(); try { state = ST_STOPPED; diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationConfig.java new file mode 100644 index 0000000000000..a8148e17cd7b1 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationConfig.java @@ -0,0 +1,25 @@ +package io.quarkus.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class ApplicationConfig { + + /** + * The name of the application. + * If not set, defaults to the name of the project (except for tests where it is not set at all). + */ + @ConfigItem + public Optional name; + + /** + * The version of the application. + * If not set, defaults to the version of the project (except for tests where it is not set at all). + */ + @ConfigItem + public Optional version; +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/CleanableExecutor.java b/core/runtime/src/main/java/io/quarkus/runtime/CleanableExecutor.java index ce7f97342bfd5..f480022a5f07e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/CleanableExecutor.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/CleanableExecutor.java @@ -27,7 +27,7 @@ * * This is only for development mode, it must not be used for production applications. * - * TODO: should this just provide a facacde that simply starts a new thread pool instead? + * TODO: should this just provide a facade that simply starts a new thread pool instead? */ public final class CleanableExecutor implements ExecutorService { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ConfigHelper.java b/core/runtime/src/main/java/io/quarkus/runtime/ConfigHelper.java deleted file mode 100644 index 015e77867c987..0000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/ConfigHelper.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.quarkus.runtime; - -import java.util.Optional; - -import org.eclipse.microprofile.config.ConfigProvider; - -public class ConfigHelper { - - private ConfigHelper() { - - } - - public static String getString(String key, String defaultValue) { - Optional val = ConfigProvider.getConfig().getOptionalValue(key, String.class); - return val.orElse(defaultValue); - } - - public static Integer getInteger(String key, int defaultValue) { - Optional val = ConfigProvider.getConfig().getOptionalValue(key, Integer.class); - return val.orElse(defaultValue); - } - - public static Boolean getBoolean(String key, boolean defaultValue) { - Optional val = ConfigProvider.getConfig().getOptionalValue(key, Boolean.class); - return val.orElse(defaultValue); - } -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TopLevelRootConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/TopLevelRootConfig.java new file mode 100644 index 0000000000000..72a3f9b01d62f --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/TopLevelRootConfig.java @@ -0,0 +1,21 @@ +package io.quarkus.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * This is used currently only to suppress warnings about unknown properties + * when the user supplies something like: -Dquarkus.profile=someProfile + * + * TODO refactor code to actually use these values + */ +@ConfigRoot(name = ConfigItem.PARENT, phase = ConfigPhase.RUN_TIME) +public class TopLevelRootConfig { + + /** + * Profile that will be active when Quarkus launches + */ + @ConfigItem(defaultValue = "prod") + String profile; +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractDelegatingConfigSource.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractDelegatingConfigSource.java index 99bb69b3c0af0..95d3feddfef55 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractDelegatingConfigSource.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractDelegatingConfigSource.java @@ -1,5 +1,6 @@ package io.quarkus.runtime.configuration; +import java.io.Serializable; import java.util.Map; import java.util.Set; @@ -12,7 +13,8 @@ /** * A base class for configuration sources which delegate to other configuration sources. */ -public abstract class AbstractDelegatingConfigSource implements ConfigSource { +public abstract class AbstractDelegatingConfigSource implements ConfigSource, Serializable { + private static final long serialVersionUID = -6636734120743034580L; protected final ConfigSource delegate; private Map propertiesMap; diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractRawDefaultConfigSource.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractRawDefaultConfigSource.java index 3e2f4915cd5ff..3b1d418c9a72b 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractRawDefaultConfigSource.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/AbstractRawDefaultConfigSource.java @@ -1,5 +1,6 @@ package io.quarkus.runtime.configuration; +import java.io.Serializable; import java.util.Collections; import java.util.Map; @@ -8,7 +9,9 @@ /** * The base class for the config source that yields the 'raw' default values. */ -public abstract class AbstractRawDefaultConfigSource implements ConfigSource { +public abstract class AbstractRawDefaultConfigSource implements ConfigSource, Serializable { + private static final long serialVersionUID = 2524612253582530249L; + protected AbstractRawDefaultConfigSource() { } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/BuildTimeConfigFactory.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/BuildTimeConfigFactory.java deleted file mode 100644 index 6f862d1c18fd9..0000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/BuildTimeConfigFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.quarkus.runtime.configuration; - -import java.io.IOError; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Enumeration; -import java.util.Properties; - -import org.eclipse.microprofile.config.spi.ConfigSource; - -import io.smallrye.config.PropertiesConfigSource; - -/** - * - */ -public final class BuildTimeConfigFactory { - - public static final String BUILD_TIME_CONFIG_NAME = "META-INF/build-config.properties"; - - private BuildTimeConfigFactory() { - } - - public static ConfigSource getBuildTimeConfigSource() { - Properties properties = new Properties(); - final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - try { - final Enumeration resources = classLoader.getResources(BUILD_TIME_CONFIG_NAME); - if (resources.hasMoreElements()) { - final URL url = resources.nextElement(); - try (InputStream is = url.openStream()) { - if (is != null) { - try (InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) { - properties.load(isr); - } - } - } - } - return new PropertiesConfigSource(properties, "Build time configuration"); - } catch (IOException e) { - throw new IOError(e); - } - } -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/CidrAddressConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/CidrAddressConverter.java index b99781b8f35fc..2443f5cec5e42 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/CidrAddressConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/CidrAddressConverter.java @@ -2,6 +2,8 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; + import javax.annotation.Priority; import org.eclipse.microprofile.config.spi.Converter; @@ -12,10 +14,13 @@ * A converter which converts a CIDR address into an instance of {@link CidrAddress}. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public class CidrAddressConverter implements Converter { +public class CidrAddressConverter implements Converter, Serializable { + + private static final long serialVersionUID = 2023552088048952902L; @Override - public CidrAddress convert(final String value) { + public CidrAddress convert(String value) { + value = value.trim(); if (value.isEmpty()) { return null; } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java new file mode 100644 index 0000000000000..1054e6218e871 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java @@ -0,0 +1,88 @@ +package io.quarkus.runtime.configuration; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.graalvm.nativeimage.ImageInfo; +import org.jboss.logging.Logger; + +import com.oracle.svm.core.annotate.RecomputeFieldValue; + +/** + * Utility methods to log configuration problems. + */ +public final class ConfigDiagnostic { + private static final Logger log = Logger.getLogger("io.quarkus.config"); + + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) + private static final List errorsMessages = new CopyOnWriteArrayList<>(); + + private ConfigDiagnostic() { + } + + public static void invalidValue(String name, IllegalArgumentException ex) { + final String message = ex.getMessage(); + final String loggedMessage = String.format("An invalid value was given for configuration key \"%s\": %s", name, + message == null ? ex.toString() : message); + log.error(loggedMessage); + errorsMessages.add(loggedMessage); + } + + public static void missingValue(String name, NoSuchElementException ex) { + final String message = ex.getMessage(); + final String loggedMessage = String.format("Configuration key \"%s\" is required, but its value is empty/missing: %s", + name, + message == null ? ex.toString() : message); + log.error(loggedMessage); + errorsMessages.add(loggedMessage); + } + + public static void duplicate(String name) { + final String loggedMessage = String.format("Configuration key \"%s\" was specified more than once", name); + errorsMessages.add(loggedMessage); + } + + public static void deprecated(String name) { + log.warnf("Configuration key \"%s\" is deprecated", name); + } + + public static void unknown(String name) { + log.warnf("Unrecognized configuration key \"%s\" was provided; it will be ignored", name); + } + + public static void unknown(NameIterator name) { + unknown(name.getName()); + } + + public static void unknownRunTime(String name) { + if (ImageInfo.inImageRuntimeCode()) { + // only warn at run time for native images, otherwise the user will get warned twice for every property + log.warnf("Unrecognized configuration key \"%s\" was provided; it will be ignored", name); + } + } + + public static void unknownRunTime(NameIterator name) { + unknownRunTime(name.getName()); + } + + /** + * Determine if a fatal configuration error has occurred. + * + * @return {@code true} if a fatal configuration error has occurred + */ + public static boolean isError() { + return !errorsMessages.isEmpty(); + } + + /** + * Reset the config error status (for e.g. testing). + */ + public static void resetError() { + errorsMessages.clear(); + } + + public static String getNiceErrorMessage() { + return String.join("\n", errorsMessages); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigInstantiator.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigInstantiator.java index ad3d262b6ffbb..33daecaacfb36 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigInstantiator.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigInstantiator.java @@ -1,23 +1,24 @@ package io.quarkus.runtime.configuration; +import java.lang.reflect.Array; import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.NoSuchElementException; import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; import java.util.Set; -import java.util.regex.Pattern; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.Converter; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; +import io.smallrye.config.Converters; import io.smallrye.config.SmallRyeConfig; /** @@ -30,7 +31,6 @@ */ public class ConfigInstantiator { - private static final Pattern COMMA_PATTERN = Pattern.compile(","); // certain well-known classname suffixes that we support private static Set supportedClassNameSuffix; @@ -71,54 +71,15 @@ private static void handleObject(String prefix, Object o, SmallRyeConfig config) String name = configItem.name(); if (name.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) { name = dashify(field.getName()); + } else if (name.equals(ConfigItem.ELEMENT_NAME)) { + name = field.getName(); } String fullName = prefix + "." + name; - String defaultValue = configItem.defaultValue(); - if (defaultValue.equals(ConfigItem.NO_DEFAULT)) { - defaultValue = null; - } final Type genericType = field.getGenericType(); - Optional val; - final boolean fieldIsOptional = fieldClass.equals(Optional.class); - final boolean fieldIsList = fieldClass.equals(List.class); - if (fieldIsOptional) { - Class actualType = (Class) ((ParameterizedType) genericType) - .getActualTypeArguments()[0]; - val = config.getOptionalValue(fullName, actualType); - } else if (fieldIsList) { - Class actualType = (Class) ((ParameterizedType) genericType) - .getActualTypeArguments()[0]; - val = config.getOptionalValues(fullName, actualType, ArrayList::new); - } else { - val = config.getOptionalValue(fullName, fieldClass); - } - if (val.isPresent()) { - field.set(o, fieldIsOptional ? val : val.get()); - } else if (defaultValue != null) { - if (fieldIsList) { - Class listType = (Class) ((ParameterizedType) genericType) - .getActualTypeArguments()[0]; - String[] parts = COMMA_PATTERN.split(defaultValue); - List list = new ArrayList<>(); - for (String i : parts) { - list.add(config.convert(i, listType)); - } - field.set(o, list); - } else if (fieldIsOptional) { - Class optionalType = (Class) ((ParameterizedType) genericType) - .getActualTypeArguments()[0]; - field.set(o, Optional.of(config.convert(defaultValue, optionalType))); - } else { - field.set(o, config.convert(defaultValue, fieldClass)); - } - } else if (fieldIsOptional) { - field.set(o, Optional.empty()); - } else if (fieldClass.equals(OptionalInt.class)) { - field.set(o, OptionalInt.empty()); - } else if (fieldClass.equals(OptionalDouble.class)) { - field.set(o, OptionalDouble.empty()); - } else if (fieldClass.equals(OptionalLong.class)) { - field.set(o, OptionalLong.empty()); + final Converter conv = getConverterFor(genericType); + try { + field.set(o, config.getValue(fullName, conv)); + } catch (NoSuchElementException ignored) { } } } @@ -127,6 +88,40 @@ private static void handleObject(String prefix, Object o, SmallRyeConfig config) } } + private static Converter getConverterFor(Type type) { + // hopefully this is enough + final SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig(); + Class rawType = rawTypeOf(type); + if (rawType == Optional.class) { + return Converters.newOptionalConverter(getConverterFor(typeOfParameter(type, 0))); + } else if (rawType == List.class) { + return Converters.newCollectionConverter(getConverterFor(typeOfParameter(type, 0)), ArrayList::new); + } else { + return config.getConverter(rawTypeOf(type)); + } + } + + // cribbed from io.quarkus.deployment.util.ReflectUtil + private static Class rawTypeOf(final Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return rawTypeOf(((ParameterizedType) type).getRawType()); + } else if (type instanceof GenericArrayType) { + return Array.newInstance(rawTypeOf(((GenericArrayType) type).getGenericComponentType()), 0).getClass(); + } else { + throw new IllegalArgumentException("Type has no raw type class: " + type); + } + } + + static Type typeOfParameter(final Type type, final int paramIdx) { + if (type instanceof ParameterizedType) { + return ((ParameterizedType) type).getActualTypeArguments()[paramIdx]; + } else { + throw new IllegalArgumentException("Type is not parameterized: " + type); + } + } + // Configuration keys are normally derived from the field names that they are tied to. // This is done by de-camel-casing the name and then joining the segments with hyphens (-). // Some examples: diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java index 50295d1a0b01c..6d260f36ff452 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java @@ -1,196 +1,124 @@ package io.quarkus.runtime.configuration; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; import java.util.function.IntFunction; -import java.util.stream.Collectors; +import java.util.regex.Pattern; -import org.eclipse.microprofile.config.spi.Converter; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; -import io.smallrye.config.SmallRyeConfig; -import io.smallrye.config.StringUtil; +import io.smallrye.config.PropertiesConfigSourceProvider; +import io.smallrye.config.SmallRyeConfigBuilder; /** * */ public final class ConfigUtils { - private static final Map> EXPLICIT_RUNTIME_CONVERTERS_CACHE = new HashMap<>(); - private ConfigUtils() { } - /** - * This method replicates the logic of {@link SmallRyeConfig#getValues(String, Class, IntFunction)} for the given - * default value string. - * - * @param config the config instance (must not be {@code null}) - * @param defaultValue the default value string (must not be {@code null}) - * @param itemType the item type class (must not be {@code null}) - * @param converterClass - The converter class to use - * @param collectionFactory the collection factory (must not be {@code null}) - * @param the item type - * @param the collection type - * @return the collection (not {@code null}) - */ - public static > C getDefaults(SmallRyeConfig config, String defaultValue, Class itemType, - Class> converterClass, - IntFunction collectionFactory) { - final String[] items = Arrays.stream(StringUtil.split(defaultValue)).filter(s -> !s.isEmpty()).toArray(String[]::new); - final C collection = collectionFactory.apply(items.length); - for (String item : items) { - if (converterClass == null) { - collection.add(config.convert(item, itemType)); - } else { - final Converter converter = getConverterOfType(itemType, converterClass); - final String rawValue = config.convert(item, String.class); - collection.add(converter.convert(rawValue)); - } - } - - return collection; + public static IntFunction> listFactory() { + return ArrayList::new; } - /** - * Retrieve the value of a given config name from Configuration object. Converter the value to an appropriate type using the - * given converter. - * - * @param config - Configuration object (must not be {@code null}) - * @param configName - the property name (must not be {@code null}) - * @param objectType - the type of the object (must not be {@code null}) - * @param converterClass - The converter class to use - * @return the value in appropriate type - */ - public static T getValue(SmallRyeConfig config, String configName, Class objectType, - Class> converterClass) { - if (converterClass == null) { - return config.getValue(configName, objectType); - } - - final Converter converter = getConverterOfType(objectType, converterClass); - final String rawValue = config.getValue(configName, String.class); - return converter.convert(rawValue); + public static IntFunction> setFactory() { + return LinkedHashSet::new; } - /** - * Retrieve the Optional value of a property represented by the given config name. Converter the value to an appropriate - * type using the given converter. - * - * @param config - Configuration object (must not be {@code null}) - * @param configName - the property name (must not be {@code null}) - * @param objectType - the type of the object (must not be {@code null}) - * @param converterClass - The converter class to use - * @return Optional value of appropriate type - */ - public static Optional getOptionalValue(SmallRyeConfig config, String configName, Class objectType, - Class> converterClass) { - if (converterClass == null) { - return config.getOptionalValue(configName, objectType); - } - - final Converter converter = getConverterOfType(objectType, converterClass); - final String rawValue = config.getValue(configName, String.class); - return Optional.ofNullable(converter.convert(rawValue)); + public static IntFunction> sortedSetFactory() { + return size -> new TreeSet<>(); } /** - * Retrieve the value of a given config name from Configuration object. Converter the value to an appropriate type using the - * given converter. + * Get the basic configuration builder. * - * @param config - Configuration object (must not be {@code null}) - * @param configName - the property name (must not be {@code null}) - * @param objectType - the type of the object (must not be {@code null}) - * @param converterClass - The converter class to use - * @return the values in appropriate type + * @param runTime {@code true} if the configuration is run time, {@code false} if build time + * @return the configuration builder */ - public static ArrayList getValues(SmallRyeConfig config, String configName, Class objectType, - Class> converterClass) { - if (converterClass == null) { - return config.getValues(configName, objectType, ArrayListFactory.getInstance()); + public static SmallRyeConfigBuilder configBuilder(final boolean runTime) { + final SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder(); + final ApplicationPropertiesConfigSource.InFileSystem inFileSystem = new ApplicationPropertiesConfigSource.InFileSystem(); + final ApplicationPropertiesConfigSource.InJar inJar = new ApplicationPropertiesConfigSource.InJar(); + builder.withSources(inFileSystem, inJar); + final ExpandingConfigSource.Cache cache = new ExpandingConfigSource.Cache(); + builder.withWrapper(ExpandingConfigSource.wrapper(cache)); + builder.withWrapper(DeploymentProfileConfigSource.wrapper()); + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (runTime) { + builder.addDefaultSources(); + } else { + final List sources = new ArrayList<>(); + sources.addAll(new PropertiesConfigSourceProvider("META-INF/microprofile-config.properties", true, classLoader) + .getConfigSources(classLoader)); + // required by spec... + sources.addAll( + new PropertiesConfigSourceProvider("WEB-INF/classes/META-INF/microprofile-config.properties", true, + classLoader).getConfigSources(classLoader)); + sources.add(new EnvConfigSource()); + sources.add(new SysPropConfigSource()); + builder.withSources(sources.toArray(new ConfigSource[0])); } - - final Converter converter = getConverterOfType(objectType, converterClass); - final ArrayList rawValues = config.getValues(configName, String.class, ArrayListFactory.getInstance()); - return rawValues.parallelStream().map(converter::convert).collect(Collectors.toCollection(ArrayList::new)); + builder.addDiscoveredSources(); + builder.addDiscoveredConverters(); + return builder; } /** - * Converter the value to an appropriate type using the given converter. + * Add a configuration source provider to the builder. * - * @param config - Configuration object (must not be {@code null}) - * @param value - the value (must not be {@code null}) - * @param objectType - the type of the object (must not be {@code null}) - * @param converterClass - The converter class to use - * @return the value + * @param builder the builder + * @param provider the provider to add */ - public static T convert(SmallRyeConfig config, String value, Class objectType, - Class> converterClass) { - if (converterClass == null) { - return config.convert(value, objectType); + public static void addSourceProvider(SmallRyeConfigBuilder builder, ConfigSourceProvider provider) { + final Iterable sources = provider.getConfigSources(Thread.currentThread().getContextClassLoader()); + for (ConfigSource source : sources) { + builder.withSources(source); } - - final Converter converter = getConverterOfType(objectType, converterClass); - final String rawValue = config.convert(value, String.class); - return converter.convert(rawValue); } - private static Converter getConverterOfType(Class type, Class> converterType) { - @SuppressWarnings("unchecked") - final Converter converter = (Converter) EXPLICIT_RUNTIME_CONVERTERS_CACHE - .get(new ConverterClassHolder(type, converterType)); - if (converter != null) { - return converter; - } - - // build time converter no need to be cached - return newConverterInstance(type, converterType); - } + static final class EnvConfigSource implements ConfigSource { + static final Pattern REP_PATTERN = Pattern.compile("[^a-zA-Z0-9_]"); - public static Converter newConverterInstance(Class type, Class> converterClass) { - // todo: this gets cleaned up with the SmallRye Config update - if (HyphenateEnumConverter.class.equals(converterClass)) { - @SuppressWarnings("unchecked") - final Converter converter = new HyphenateEnumConverter(type); - return converter; + public Map getProperties() { + return Collections.emptyMap(); } - try { - return converterClass.getConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { - throw new IllegalArgumentException(e); + public String getValue(final String propertyName) { + return System.getenv(REP_PATTERN.matcher(propertyName.toUpperCase(Locale.ROOT)).replaceAll("_")); } - } - public static void populateExplicitRuntimeConverter(Class typeClass, Class> converterType, - Converter converter) { - final Class type = getWrapperClass(typeClass); - EXPLICIT_RUNTIME_CONVERTERS_CACHE.put(new ConverterClassHolder(type, converterType), converter); + public String getName() { + return "System environment"; + } } - private static Class getWrapperClass(Class type) { - if (type == Integer.TYPE) { - return Integer.class; + static final class SysPropConfigSource implements ConfigSource { + public Map getProperties() { + Map output = new TreeMap<>(); + for (Map.Entry entry : System.getProperties().entrySet()) { + String key = (String) entry.getKey(); + if (key.startsWith("quarkus.")) { + output.put(key, entry.getValue().toString()); + } + } + return output; } - if (type == Long.TYPE) { - return Long.class; - } - if (type == Boolean.TYPE) { - return Boolean.class; - } - if (type == Float.TYPE) { - return Float.class; + public String getValue(final String propertyName) { + return System.getProperty(propertyName); } - if (type == Double.TYPE) { - return Double.class; + public String getName() { + return "System properties"; } - - return type; } - } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigurationException.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigurationException.java new file mode 100644 index 0000000000000..cb5706fb9e397 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigurationException.java @@ -0,0 +1,46 @@ +package io.quarkus.runtime.configuration; + +/** + * An exception indicating that a configuration failure has occurred. + */ +public class ConfigurationException extends RuntimeException { + private static final long serialVersionUID = 4445679764085720090L; + + /** + * Constructs a new {@code ConfigurationException} instance. The message is left blank ({@code null}), and no + * cause is specified. + */ + public ConfigurationException() { + } + + /** + * Constructs a new {@code ConfigurationException} instance with an initial message. No + * cause is specified. + * + * @param msg the message + */ + public ConfigurationException(final String msg) { + super(msg); + } + + /** + * Constructs a new {@code ConfigurationException} instance with an initial cause. If + * a non-{@code null} cause is specified, its message is used to initialize the message of this + * {@code ConfigurationException}; otherwise the message is left blank ({@code null}). + * + * @param cause the cause + */ + public ConfigurationException(final Throwable cause) { + super(cause); + } + + /** + * Constructs a new {@code ConfigurationException} instance with an initial message and cause. + * + * @param msg the message + * @param cause the cause + */ + public ConfigurationException(final String msg, final Throwable cause) { + super(msg, cause); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConverterSupport.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConverterSupport.java index 5e66365e41506..748e976d4fa3d 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConverterSupport.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConverterSupport.java @@ -1,36 +1,14 @@ package io.quarkus.runtime.configuration; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.function.Consumer; - import javax.annotation.Priority; -import org.eclipse.microprofile.config.spi.ConfigBuilder; import org.eclipse.microprofile.config.spi.Converter; -import org.jboss.logging.Logger; - -import io.smallrye.config.Converters; /** - * This small utility class is a tool which helps populating SmallRye {@link ConfigBuilder} with - * {@link Converter} implementations loaded from {@link ServiceLoader}. + * This small utility class holds constants relevant to configuration converters. */ public class ConverterSupport { - private static final Logger LOG = Logger.getLogger(ConverterSupport.class); - - /** - * A list of {@link ConverterItem} which will be loaded in static initialization. This needs - * to be static so we can load {@link Converter} implementations without producing reflective - * class build items in the deployment time. The {@link ConverterItem} instances uses generic - * {@link Object} type to avoid typecast errors from compiler. - */ - private static final List> CONVERTERS = getConverters(); - /** * Default {@link Converter} priority with value {@value #DEFAULT_SMALLRYE_CONVERTER_PRIORITY} * to be used for all discovered converters in case when no {@link Priority} annotation is @@ -48,124 +26,7 @@ public class ConverterSupport { */ public static final int DEFAULT_QUARKUS_CONVERTER_PRIORITY = 200; - /** - * Populates given {@link ConfigBuilder} with all {@link Converter} implementations loaded from - * the {@link ServiceLoader}. - * - * @param builder the {@link ConfigBuilder} - */ - public static void populateConverters(final ConfigBuilder builder) { - CONVERTERS.forEach(addConverterTo(builder)); - } - - /** - * Get {@link Converter} priority by looking for a {@link Priority} annotation which can be put - * on the converter type. If no {@link Priority} annotation is found a default priority of - * {@value #DEFAULT_SMALLRYE_CONVERTER_PRIORITY} is returned. - * - * @param converterClass - * @return - */ - static int getConverterPriority(final Class> converterClass) { - return Optional - .ofNullable(converterClass.getAnnotation(Priority.class)) - .map(Priority::value) - .orElse(DEFAULT_SMALLRYE_CONVERTER_PRIORITY); - } - - /** - * Converts {@link Converter} instance into {@link ConverterItem}. - * - * @param the converter conversion type - * @param converter the converter instance - * @return New {@link ConverterItem} which wraps given {@link Converter} and related metadata - */ - @SuppressWarnings("unchecked") - private static ConverterItem converterToItem(final Converter converter) { - final Class> converterClass = (Class>) converter.getClass(); - final Type genericType = Converters.getConverterType(converterClass); - if (!(genericType instanceof Class)) { - throw new IllegalArgumentException("General converters may not convert generic types"); - } - final Class convertedType = (Class) genericType; - final int priority = getConverterPriority(converterClass); - return new ConverterItem(convertedType, converter, priority); - } - - /** - * @return A {@link List} of {@link ConverterItem} loaded from {@link ServiceLoader} - */ - @SuppressWarnings("unchecked") - private static List> getConverters() { - final List> items = new ArrayList<>(); - for (Converter converter : ServiceLoader.load(Converter.class)) { - items.add((ConverterItem) converterToItem(converter)); - } - return items; - } - - /** - * Create a {@link Consumer} which consumes {@link ConverterItem} wrapping {@link Converter} - * (and related metadata) and adds it to the given {@link ConfigBuilder}. - * - * @param the {@link Converter} conversion type - * @param builder - * @return A {@link ConverterItem} {@link Consumer} which populates {@link ConfigBuilder} - */ - private static Consumer> addConverterTo(final ConfigBuilder builder) { - return item -> { - - final Class type = item.getConvertedType(); - final Converter converter = item.getConverter(); - final int priority = item.getPriority(); - - LOG.debugf("Populate SmallRye config builder with converter for %s of priority %s", type, priority); - - builder.withConverter(type, priority, converter); - }; - } - private ConverterSupport() { // this is utility class } - - /** - * This class wraps {@link Converter} and related metadata, i.e. a {@link Converter} conversion - * type and its priority. - * - * @param the {@link Converter} conversion type - */ - private static final class ConverterItem { - - final Class convertedType; - final Converter converter; - final int priority; - - public ConverterItem(final Class convertedType, final Converter converter, final int priority) { - this.convertedType = convertedType; - this.converter = converter; - this.priority = priority; - } - - /** - * @return {@link Converter} conversion type - */ - public Class getConvertedType() { - return convertedType; - } - - /** - * @return A {@link Converter} this item wraps - */ - public Converter getConverter() { - return converter; - } - - /** - * @return A {@link Converter} priority - */ - public int getPriority() { - return priority; - } - } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DefaultConfigSource.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DefaultConfigSource.java deleted file mode 100644 index 40cbe8f7d02da..0000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DefaultConfigSource.java +++ /dev/null @@ -1,53 +0,0 @@ -package io.quarkus.runtime.configuration; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Enumeration; -import java.util.Map; -import java.util.Properties; - -import io.smallrye.config.PropertiesConfigSource; - -/** - * The default values run time configuration source. - */ -public final class DefaultConfigSource extends PropertiesConfigSource { - private static final long serialVersionUID = -6482737535291300045L; - - public static final String DEFAULT_CONFIG_PROPERTIES_NAME = "META-INF/quarkus-default-config.properties"; - - /** - * Construct a new instance. - */ - public DefaultConfigSource() { - super(getMap(), "Default configuration values", 0); - } - - @SuppressWarnings("unchecked") - private static Map getMap() { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - if (cl == null) { - cl = DefaultConfigSource.class.getClassLoader(); - } - try { - final Properties p = new Properties(); - // work around #1477 - final Enumeration resources = cl == null ? ClassLoader.getSystemResources(DEFAULT_CONFIG_PROPERTIES_NAME) - : cl.getResources(DEFAULT_CONFIG_PROPERTIES_NAME); - if (resources.hasMoreElements()) { - final URL url = resources.nextElement(); - try (InputStream is = url.openStream()) { - try (InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) { - p.load(isr); - } - } - } - return (Map) p; - } catch (IOException e) { - throw new IllegalStateException("Cannot read default configuration", e); - } - } -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeploymentProfileConfigSource.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeploymentProfileConfigSource.java index 1eb4059d75393..c7da037b9d170 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeploymentProfileConfigSource.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeploymentProfileConfigSource.java @@ -1,5 +1,7 @@ package io.quarkus.runtime.configuration; +import java.io.ObjectStreamException; +import java.io.Serializable; import java.util.HashSet; import java.util.Set; import java.util.function.UnaryOperator; @@ -11,6 +13,7 @@ * A configuration source which supports deployment profiles. */ public class DeploymentProfileConfigSource extends AbstractDelegatingConfigSource { + private static final long serialVersionUID = -8001338475089294128L; private final String profilePrefix; @@ -35,6 +38,26 @@ public DeploymentProfileConfigSource(final ConfigSource delegate, final String p profilePrefix = "%" + profileName + "."; } + Object writeReplace() throws ObjectStreamException { + return new Ser(delegate, profilePrefix); + } + + static final class Ser implements Serializable { + private static final long serialVersionUID = -4618790131794331510L; + + final ConfigSource d; + final String p; + + Ser(final ConfigSource d, String p) { + this.d = d; + this.p = p; + } + + Object readResolve() { + return new DeploymentProfileConfigSource(d, p); + } + } + public Set getPropertyNames() { Set propertyNames = delegate.getPropertyNames(); //if a key is only present in a profile we still want the unprofiled key name to show up @@ -61,4 +84,9 @@ public String getValue(final String name) { public String getName() { return delegate.getName(); } + + public String toString() { + return "DeploymentProfileConfigSource[profile=" + profilePrefix + ",delegate=" + getDelegate() + ",ord=" + getOrdinal() + + "]"; + } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java index f356eb22343e0..8c04d55c5e4aa 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java @@ -2,6 +2,7 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; import java.time.Duration; import java.time.format.DateTimeParseException; import java.util.regex.Pattern; @@ -14,7 +15,8 @@ * A converter for a {@link Duration} interface. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public class DurationConverter implements Converter { +public class DurationConverter implements Converter, Serializable { + private static final long serialVersionUID = 7499347081928776532L; private static final String PERIOD_OF_TIME = "PT"; private static final Pattern DIGITS = Pattern.compile("^[-+]?\\d+$"); private static final Pattern START_WITH_DIGITS = Pattern.compile("^[-+]?\\d+.*"); @@ -32,6 +34,10 @@ public DurationConverter() { */ @Override public Duration convert(String value) { + value = value.trim(); + if (value.isEmpty()) { + return null; + } if (DIGITS.asPredicate().test(value)) { return Duration.ofSeconds(Long.valueOf(value)); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ExpandingConfigSource.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ExpandingConfigSource.java index aefcc7dfb77e3..b5aeb7e9c8460 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ExpandingConfigSource.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ExpandingConfigSource.java @@ -1,5 +1,7 @@ package io.quarkus.runtime.configuration; +import java.io.ObjectStreamException; +import java.io.Serializable; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.UnaryOperator; @@ -13,6 +15,7 @@ */ public class ExpandingConfigSource extends AbstractDelegatingConfigSource { + private static final long serialVersionUID = 1075000015425893741L; private static final ThreadLocal NO_EXPAND = new ThreadLocal<>(); public static UnaryOperator wrapper(Cache cache) { @@ -44,6 +47,24 @@ public String getValue(final String propertyName) { return isExpanding() ? expand(delegateValue) : delegateValue; } + Object writeReplace() throws ObjectStreamException { + return new Ser(delegate); + } + + static final class Ser implements Serializable { + private static final long serialVersionUID = 3633535720479375279L; + + final ConfigSource d; + + Ser(final ConfigSource d) { + this.d = d; + } + + Object readResolve() { + return new ExpandingConfigSource(d, new Cache()); + } + } + String expand(final String value) { return expandValue(value, cache); } @@ -52,6 +73,10 @@ public void flush() { cache.flush(); } + public String toString() { + return "ExpandingConfigSource[delegate=" + getDelegate() + ",ord=" + getOrdinal() + "]"; + } + private static boolean isExpanding() { return NO_EXPAND.get() != Boolean.TRUE; } @@ -79,7 +104,9 @@ public static String expandValue(String value, Cache cache) { /** * An expression cache to use with {@link ExpandingConfigSource}. */ - public static final class Cache { + public static final class Cache implements Serializable { + private static final long serialVersionUID = 6111143168103886992L; + // this is a cache of compiled expressions, NOT a cache of expanded values final ConcurrentHashMap exprCache = new ConcurrentHashMap<>(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/HyphenateEnumConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/HyphenateEnumConverter.java index a1108704612fc..81e7edcb9c174 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/HyphenateEnumConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/HyphenateEnumConverter.java @@ -1,5 +1,6 @@ package io.quarkus.runtime.configuration; +import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; @@ -7,12 +8,15 @@ import org.eclipse.microprofile.config.spi.Converter; +import io.quarkus.runtime.util.StringUtil; + /** - * A converter for hyphenated enums + * A converter for hyphenated enums. */ -final public class HyphenateEnumConverter> implements Converter { +public final class HyphenateEnumConverter> implements Converter, Serializable { private static final String HYPHEN = "-"; private static final Pattern PATTERN = Pattern.compile("([-_]+)"); + private static final long serialVersionUID = 5675903245398498741L; private final Class enumType; private final Map values = new HashMap<>(); @@ -27,13 +31,16 @@ public HyphenateEnumConverter(Class enumType) { } } + public static > HyphenateEnumConverter of(Class enumType) { + return new HyphenateEnumConverter(enumType); + } + @Override public E convert(String value) { - if (value == null || value.trim().isEmpty()) { + value = value.trim(); + if (value.isEmpty()) { return null; } - - value = value.trim(); final String hyphenatedValue = hyphenate(value); final Enum enumValue = values.get(hyphenatedValue); @@ -46,7 +53,7 @@ public E convert(String value) { private String hyphenate(String value) { StringBuffer target = new StringBuffer(); - String hyphenate = io.quarkus.runtime.util.StringUtil.hyphenate(value); + String hyphenate = StringUtil.hyphenate(value); Matcher matcher = PATTERN.matcher(hyphenate); while (matcher.find()) { matcher.appendReplacement(target, HYPHEN); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetAddressConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetAddressConverter.java index 94c5c498df6ab..a1bbbcd1575cf 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetAddressConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetAddressConverter.java @@ -2,6 +2,7 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; import java.net.InetAddress; import java.net.UnknownHostException; @@ -14,10 +15,13 @@ * A converter which produces values of type {@link InetAddress}. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public class InetAddressConverter implements Converter { +public class InetAddressConverter implements Converter, Serializable { + + private static final long serialVersionUID = 4539214213710330204L; @Override - public InetAddress convert(final String value) { + public InetAddress convert(String value) { + value = value.trim(); if (value.isEmpty()) { return null; } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetSocketAddressConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetSocketAddressConverter.java index 42fa13083715d..d6dad37ef5e7e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetSocketAddressConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/InetSocketAddressConverter.java @@ -2,6 +2,7 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -16,10 +17,13 @@ * an unresolved instance is returned. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public class InetSocketAddressConverter implements Converter { +public class InetSocketAddressConverter implements Converter, Serializable { + + private static final long serialVersionUID = 1928336763333858343L; @Override - public InetSocketAddress convert(final String value) { + public InetSocketAddress convert(String value) { + value = value.trim(); if (value.isEmpty()) { return null; } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/MemorySizeConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/MemorySizeConverter.java index eaa1858e69193..f88cc13f87798 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/MemorySizeConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/MemorySizeConverter.java @@ -2,6 +2,7 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; import java.math.BigInteger; import java.util.HashMap; import java.util.Map; @@ -16,10 +17,11 @@ * A converter to support data sizes. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public class MemorySizeConverter implements Converter { +public class MemorySizeConverter implements Converter, Serializable { private static final Pattern MEMORY_SIZE_PATTERN = Pattern.compile("^(\\d+)([BbKkMmGgTtPpEeZzYy]?)$"); static final BigInteger KILO_BYTES = BigInteger.valueOf(1024); private static final Map MEMORY_SIZE_MULTIPLIERS; + private static final long serialVersionUID = -1988485929047973068L; static { MEMORY_SIZE_MULTIPLIERS = new HashMap<>(); @@ -42,6 +44,10 @@ public class MemorySizeConverter implements Converter { * @return {@link MemorySize} - a memory size represented by the given value */ public MemorySize convert(String value) { + value = value.trim(); + if (value.isEmpty()) { + return null; + } Matcher matcher = MEMORY_SIZE_PATTERN.matcher(value); if (matcher.find()) { BigInteger number = new BigInteger(matcher.group(1)); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/NameIterator.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/NameIterator.java index b641f262bd090..0c68a3c2a2348 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/NameIterator.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/NameIterator.java @@ -295,6 +295,22 @@ public String getPreviousSegment() { } } + public String getAllPreviousSegments() { + final int pos = getPosition(); + if (pos == -1) { + return ""; + } + return name.substring(0, pos); + } + + public String getAllPreviousSegmentsWith(String suffix) { + final int pos = getPosition(); + if (pos == -1) { + return suffix; + } + return name.substring(0, pos) + "." + suffix; + } + public boolean hasNext() { return pos < name.length(); } @@ -316,7 +332,12 @@ public String getName() { } public String toString() { - // generated code relies on this behavior - return getName(); + if (pos == -1) { + return "*" + name; + } else if (pos == name.length()) { + return name + "*"; + } else { + return name.substring(0, pos) + '*' + name.substring(pos + 1); + } } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PathConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PathConverter.java index 0b1aaf9872c60..e113a49813308 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PathConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PathConverter.java @@ -2,6 +2,7 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; import java.nio.file.Path; import java.nio.file.Paths; @@ -13,10 +14,12 @@ * A converter for a {@link Path} interface. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public class PathConverter implements Converter { +public class PathConverter implements Converter, Serializable { + + private static final long serialVersionUID = 4452863383998867844L; @Override public Path convert(String value) { - return Paths.get(value); + return value.isEmpty() ? null : Paths.get(value); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/QuarkusConfigFactory.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/QuarkusConfigFactory.java new file mode 100644 index 0000000000000..b359cd2aac0a4 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/QuarkusConfigFactory.java @@ -0,0 +1,29 @@ +package io.quarkus.runtime.configuration; + +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigFactory; +import io.smallrye.config.SmallRyeConfigProviderResolver; + +/** + * The simple Quarkus implementation of {@link SmallRyeConfigFactory}. + */ +public final class QuarkusConfigFactory extends SmallRyeConfigFactory { + + private static volatile SmallRyeConfig config; + + /** + * Construct a new instance. Called by service loader. + */ + public QuarkusConfigFactory() { + // todo: replace with {@code provider()} post-Java 11 + } + + public SmallRyeConfig getConfigFor(final SmallRyeConfigProviderResolver configProviderResolver, + final ClassLoader classLoader) { + return config; + } + + public static void setConfig(SmallRyeConfig config) { + QuarkusConfigFactory.config = config; + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/RegexConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/RegexConverter.java index eb395a0b945b8..0f8babbc4b906 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/RegexConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/RegexConverter.java @@ -2,6 +2,7 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; import java.util.regex.Pattern; import javax.annotation.Priority; @@ -12,7 +13,9 @@ * A converter to support regular expressions. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public class RegexConverter implements Converter { +public class RegexConverter implements Converter, Serializable { + + private static final long serialVersionUID = -2627801624423530576L; /** * Construct a new instance. @@ -21,6 +24,6 @@ public RegexConverter() { } public Pattern convert(final String value) { - return Pattern.compile(value); + return value.isEmpty() ? null : Pattern.compile(value); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/SimpleConfigurationProviderResolver.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/SimpleConfigurationProviderResolver.java deleted file mode 100644 index c41510ad7bb88..0000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/SimpleConfigurationProviderResolver.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.quarkus.runtime.configuration; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigBuilder; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; - -import io.smallrye.config.SmallRyeConfigBuilder; - -/** - * A simple configuration provider. - */ -public class SimpleConfigurationProviderResolver extends ConfigProviderResolver { - - // We use a shared config - private static volatile Config config; - - public Config getConfig() { - return config; - } - - public Config getConfig(final ClassLoader loader) { - return getConfig(); - } - - public ConfigBuilder getBuilder() { - return new SmallRyeConfigBuilder(); - } - - public void registerConfig(final Config config, final ClassLoader classLoader) { - SimpleConfigurationProviderResolver.config = config; - } - - public void releaseConfig(final Config config) { - SimpleConfigurationProviderResolver.config = null; - } -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/Substitutions.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/Substitutions.java index fbf76fd31b8e2..297734c44f2a8 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/Substitutions.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/Substitutions.java @@ -1,9 +1,6 @@ package io.quarkus.runtime.configuration; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.wildfly.common.Assert; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.Delete; @@ -19,6 +16,8 @@ final class Substitutions { static final FastThreadLocalInt depth = FastThreadLocalFactory.createInt(); + // 0 = expand so that the default value is to expand + static final FastThreadLocalInt notExpanding = FastThreadLocalFactory.createInt(); @TargetClass(ConfigExpander.class) static final class Target_ConfigExpander { @@ -58,30 +57,16 @@ static final class Target_ExpandingConfigSource { @Substitute private static boolean isExpanding() { - return true; + return notExpanding.get() == 0; } @Substitute public static boolean setExpanding(boolean newValue) { - if (!newValue) - throw Assert.unsupported(); - return true; - } - } - - @TargetClass(ConfigProvider.class) - static final class Target_ConfigProvider { - @Delete - private static ConfigProviderResolver INSTANCE; - - @Substitute - public static Config getConfig() { - return ConfigProviderResolver.instance().getConfig(); - } - - @Substitute - public static Config getConfig(ClassLoader cl) { - return getConfig(); + try { + return notExpanding.get() == 0; + } finally { + notExpanding.set(newValue ? 0 : 1); + } } } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/TemporaryConfigSourceProvider.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/TemporaryConfigSourceProvider.java deleted file mode 100644 index b40200f6b81bf..0000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/TemporaryConfigSourceProvider.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.quarkus.runtime.configuration; - -import java.util.Arrays; - -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.ConfigSourceProvider; - -/** - * This is a temporary hack until the class loader mess is worked out. - */ -public class TemporaryConfigSourceProvider implements ConfigSourceProvider { - public Iterable getConfigSources(final ClassLoader forClassLoader) { - return Arrays.asList( - new ApplicationPropertiesConfigSource.InJar(), - new ApplicationPropertiesConfigSource.InFileSystem()); - } -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/graal/ConfigurationSubstitutions.java b/core/runtime/src/main/java/io/quarkus/runtime/graal/ConfigurationSubstitutions.java new file mode 100644 index 0000000000000..b7d18c0bd394e --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/graal/ConfigurationSubstitutions.java @@ -0,0 +1,49 @@ +package io.quarkus.runtime.graal; + +import org.eclipse.microprofile.config.Config; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.AlwaysInline; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import io.quarkus.runtime.configuration.QuarkusConfigFactory; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigProviderResolver; + +@TargetClass(SmallRyeConfigProviderResolver.class) +final class Target_io_smallrye_config_SmallRyeConfigProviderResolver { + @Substitute + public Config getConfig() { + final SmallRyeConfig config = Target_io_quarkus_runtime_configuration_QuarkusConfigFactory.config; + if (config == null) { + throw new IllegalStateException("No configuration is available"); + } + return config; + } + + @Substitute + @AlwaysInline("trivial") + public Config getConfig(ClassLoader classLoader) { + return getConfig(); + } + + @Substitute + public void registerConfig(Config config, ClassLoader classLoader) { + // no op + } + + @Substitute + public void releaseConfig(Config config) { + // no op + } +} + +@TargetClass(QuarkusConfigFactory.class) +final class Target_io_quarkus_runtime_configuration_QuarkusConfigFactory { + @Alias + static SmallRyeConfig config; +} + +final class ConfigurationSubstitutions { +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/CategoryConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/CategoryConfig.java index 136fc3686b219..a566601ae9905 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/CategoryConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/CategoryConfig.java @@ -1,5 +1,8 @@ package io.quarkus.runtime.logging; +import java.util.List; +import java.util.Optional; + import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -7,14 +10,21 @@ public class CategoryConfig { /** - * The minimum level that this category can be set to + * The log level level for this category */ @ConfigItem(defaultValue = "inherit") - String minLevel; + String level; /** - * The log level level for this category + * The names of the handlers to link to this category. */ - @ConfigItem(defaultValue = "inherit") - String level; + @ConfigItem + Optional> handlers; + + /** + * Specify whether or not this logger should send its output to its parent Logger + */ + @ConfigItem(defaultValue = "true") + boolean useParentHandlers; + } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java index 0c5ae163e5700..ba9558590ce3c 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java @@ -16,13 +16,14 @@ public class ConsoleConfig { boolean enable; /** - * The log format + * The log format. Note that this value will be ignored if an extension is present that takes + * control of console formatting (e.g. an XML or JSON-format extension). */ @ConfigItem(defaultValue = "%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n") String format; /** - * The console log level + * The console log level. */ @ConfigItem(defaultValue = "ALL") Level level; @@ -30,12 +31,16 @@ public class ConsoleConfig { /** * If the console logging should be in color. If undefined quarkus takes * best guess based on operating system and environment. + * Note that this value will be ignored if an extension is present that takes + * control of console formatting (e.g. an XML or JSON-format extension). */ @ConfigItem Optional color; /** - * Specify how much the colors should be darkened + * Specify how much the colors should be darkened. + * Note that this value will be ignored if an extension is present that takes + * control of console formatting (e.g. an XML or JSON-format extension). */ @ConfigItem(defaultValue = "0") int darken; diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LevelConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LevelConverter.java index b87e84404739c..8669ff5e50a86 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LevelConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LevelConverter.java @@ -2,6 +2,7 @@ import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY; +import java.io.Serializable; import java.util.logging.Level; import javax.annotation.Priority; @@ -13,7 +14,9 @@ * A simple converter for logging levels. */ @Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY) -public final class LevelConverter implements Converter { +public final class LevelConverter implements Converter, Serializable { + + private static final long serialVersionUID = 704275577610445233L; public Level convert(final String value) { if (value == null || value.isEmpty()) { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java index 8303b39a9ce3e..fc18133917f23 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogConfig.java @@ -38,6 +38,33 @@ public final class LogConfig { @ConfigDocSection public Map categories; + /** + * Console handlers. + *

+ * The named console handlers configured here can be linked on one or more categories. + */ + @ConfigItem(name = "handler.console") + @ConfigDocSection + public Map consoleHandlers; + + /** + * File handlers. + *

+ * The named file handlers configured here can be linked on one or more categories. + */ + @ConfigItem(name = "handler.file") + @ConfigDocSection + public Map fileHandlers; + + /** + * Syslog handlers. + *

+ * The named syslog handlers configured here can be linked on one or more categories. + */ + @ConfigItem(name = "handler.syslog") + @ConfigDocSection + public Map syslogHandlers; + /** * Console logging. *

diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 31d523acfb59e..0864e8ba04e17 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -6,11 +6,14 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.logging.ErrorManager; +import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; @@ -29,6 +32,7 @@ import org.jboss.logmanager.handlers.SizeRotatingFileHandler; import org.jboss.logmanager.handlers.SyslogHandler; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; /** @@ -37,24 +41,24 @@ @Recorder public class LoggingSetupRecorder { - static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); /** * ConEmu ANSI X3.64 support enabled, * used by cmder */ - static final boolean IS_CON_EMU_ANSI = IS_WINDOWS && "ON".equals(System.getenv("ConEmuANSI")); + private static final boolean IS_CON_EMU_ANSI = IS_WINDOWS && "ON".equals(System.getenv("ConEmuANSI")); /** * These tests are same as used in jansi * Source: https://github.com/fusesource/jansi/commit/bb3d538315c44f799d34fd3426f6c91c8e8dfc55 */ - static final boolean IS_CYGWIN = IS_WINDOWS + private static final boolean IS_CYGWIN = IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/") && !"cygwin".equals(System.getenv("TERM")); - static final boolean IS_MINGW_XTERM = IS_WINDOWS + private static final boolean IS_MINGW_XTERM = IS_WINDOWS && System.getenv("MSYSTEM") != null && System.getenv("MSYSTEM").startsWith("MINGW") && "xterm".equals(System.getenv("TERM")); @@ -62,56 +66,135 @@ public class LoggingSetupRecorder { public LoggingSetupRecorder() { } - public void initializeLogging(LogConfig config) { + public void initializeLogging(LogConfig config, final List>> additionalHandlers, + final List>> possibleFormatters) { + final Map categories = config.categories; final LogContext logContext = LogContext.getLogContext(); final Logger rootLogger = logContext.getLogger(""); - ErrorManager errorManager = new OnlyOnceErrorManager(); + rootLogger.setLevel(config.level.orElse(Level.INFO)); - for (Map.Entry entry : categories.entrySet()) { - final String name = entry.getKey(); - final Logger logger = logContext.getLogger(name); - final CategoryConfig categoryConfig = entry.getValue(); - if (!"inherit".equals(categoryConfig.level)) { - logger.setLevelName(categoryConfig.level); - } - } + + ErrorManager errorManager = new OnlyOnceErrorManager(); final Map filters = config.filters; List filterElements = new ArrayList<>(filters.size()); for (Entry entry : filters.entrySet()) { filterElements.add(new LogCleanupFilterElement(entry.getKey(), entry.getValue().ifStartsWith)); } - ArrayList handlers = new ArrayList<>(3); + + final ArrayList handlers = new ArrayList<>(3 + additionalHandlers.size()); + if (config.console.enable) { - errorManager = configureConsoleHandler(config.console, errorManager, filterElements, handlers); + final Handler consoleHandler = configureConsoleHandler(config.console, errorManager, filterElements, + possibleFormatters); + errorManager = consoleHandler.getErrorManager(); + handlers.add(consoleHandler); } if (config.file.enable) { - configureFileHandler(config.file, errorManager, filterElements, handlers); + handlers.add(configureFileHandler(config.file, errorManager, filterElements)); } if (config.syslog.enable) { - configureSyslogHandler(config.syslog, errorManager, filterElements, handlers); + final Handler syslogHandler = configureSyslogHandler(config.syslog, errorManager, filterElements); + if (syslogHandler != null) { + handlers.add(syslogHandler); + } + } + + Map namedHandlers = createNamedHandlers(config, possibleFormatters, errorManager, filterElements); + + for (Map.Entry entry : categories.entrySet()) { + final String name = entry.getKey(); + final Logger categoryLogger = logContext.getLogger(name); + final CategoryConfig categoryConfig = entry.getValue(); + if (!"inherit".equals(categoryConfig.level)) { + categoryLogger.setLevelName(categoryConfig.level); + } + categoryLogger.setUseParentHandlers(categoryConfig.useParentHandlers); + if (categoryConfig.handlers.isPresent()) { + addNamedHandlersToCategory(categoryConfig, namedHandlers, categoryLogger, errorManager); + } + } + + for (RuntimeValue> additionalHandler : additionalHandlers) { + final Optional optional = additionalHandler.getValue(); + if (optional.isPresent()) { + final Handler handler = optional.get(); + handler.setErrorManager(errorManager); + handler.setFilter(new LogCleanupFilter(filterElements)); + handlers.add(handler); + } } + InitialConfigurator.DELAYED_HANDLER.setAutoFlush(false); InitialConfigurator.DELAYED_HANDLER.setHandlers(handlers.toArray(EmbeddedConfigurator.NO_HANDLERS)); } - private boolean hasColorSupport() { + private static Map createNamedHandlers(LogConfig config, + List>> possibleFormatters, ErrorManager errorManager, + List filterElements) { + Map namedHandlers = new HashMap<>(); + for (Entry consoleConfigEntry : config.consoleHandlers.entrySet()) { + final Handler consoleHandler = configureConsoleHandler(consoleConfigEntry.getValue(), errorManager, filterElements, + possibleFormatters); + addToNamedHandlers(namedHandlers, consoleHandler, consoleConfigEntry.getKey()); + } + for (Entry fileConfigEntry : config.fileHandlers.entrySet()) { + final Handler fileHandler = configureFileHandler(fileConfigEntry.getValue(), errorManager, filterElements); + addToNamedHandlers(namedHandlers, fileHandler, fileConfigEntry.getKey()); + } + for (Entry sysLogConfigEntry : config.syslogHandlers.entrySet()) { + final Handler syslogHandler = configureSyslogHandler(sysLogConfigEntry.getValue(), errorManager, filterElements); + if (syslogHandler != null) { + addToNamedHandlers(namedHandlers, syslogHandler, sysLogConfigEntry.getKey()); + } + } + return namedHandlers; + } - if (IS_WINDOWS) { - if (!(IS_CON_EMU_ANSI || IS_CYGWIN || IS_MINGW_XTERM)) { - // On Windows without a known good emulator - // TODO: optimally we would check if Win32 getConsoleMode has - // ENABLE_VIRTUAL_TERMINAL_PROCESSING enabled or enable it via - // setConsoleMode. - // For now we turn it off to not generate noisy output for most - // users. - return false; + private static void addToNamedHandlers(Map namedHandlers, Handler handler, String handlerName) { + if (namedHandlers.containsKey(handlerName)) { + throw new RuntimeException(String.format("Only one handler can be configured with the same name '%s'", + handlerName)); + } + namedHandlers.put(handlerName, handler); + } + + private void addNamedHandlersToCategory(CategoryConfig categoryConfig, Map namedHandlers, + Logger categoryLogger, + ErrorManager errorManager) { + for (String categoryNamedHandler : categoryConfig.handlers.get()) { + if (namedHandlers.get(categoryNamedHandler) != null) { + categoryLogger.addHandler(namedHandlers.get(categoryNamedHandler)); } else { - // Must be on some Unix variant or ANSI-enabled windows terminal... - return true; + errorManager.error(String.format("Handler with name '%s' is linked to a category but not configured.", + categoryNamedHandler), null, ErrorManager.GENERIC_FAILURE); } + } + } + + public void initializeLoggingForImageBuild() { + if (ImageInfo.inImageBuildtimeCode()) { + final ConsoleHandler handler = new ConsoleHandler(new PatternFormatter( + "%d{HH:mm:ss,SSS} %-5p [%c{1.}] %s%e%n")); + handler.setLevel(Level.INFO); + InitialConfigurator.DELAYED_HANDLER.setAutoFlush(false); + InitialConfigurator.DELAYED_HANDLER.setHandlers(new Handler[] { handler }); + } + } + + private static boolean hasColorSupport() { + + if (IS_WINDOWS) { + // On Windows without a known good emulator + // TODO: optimally we would check if Win32 getConsoleMode has + // ENABLE_VIRTUAL_TERMINAL_PROCESSING enabled or enable it via + // setConsoleMode. + // For now we turn it off to not generate noisy output for most + // users. + // Must be on some Unix variant or ANSI-enabled windows terminal... + return IS_CON_EMU_ANSI || IS_CYGWIN || IS_MINGW_XTERM; } else { // on sane operating systems having a console is a good indicator // you are attached to a TTY with colors. @@ -119,33 +202,44 @@ private boolean hasColorSupport() { } } - private ErrorManager configureConsoleHandler(ConsoleConfig config, ErrorManager errorManager, - List filterElements, ArrayList handlers) { - final PatternFormatter formatter; - if (config.color.orElse(hasColorSupport())) { - formatter = new ColorPatternFormatter(config.darken, config.format); - } else { - formatter = new PatternFormatter(config.format); + private static Handler configureConsoleHandler(final ConsoleConfig config, final ErrorManager defaultErrorManager, + final List filterElements, + final List>> possibleFormatters) { + Formatter formatter = null; + boolean formatterWarning = false; + for (RuntimeValue> value : possibleFormatters) { + if (formatter != null) { + formatterWarning = true; + } + final Optional val = value.getValue(); + if (val.isPresent()) { + formatter = val.get(); + } } - final ConsoleHandler handler = new ConsoleHandler(formatter); - handler.setLevel(config.level); - handler.setErrorManager(errorManager); - handler.setFilter(new LogCleanupFilter(filterElements)); - if (config.async.enable) { - final AsyncHandler asyncHandler = new AsyncHandler(config.async.queueLength); - asyncHandler.setOverflowAction(config.async.overflow); - asyncHandler.addHandler(handler); - asyncHandler.setLevel(config.level); - handlers.add(asyncHandler); - } else { - handlers.add(handler); + if (formatter == null) { + if (config.color.orElse(hasColorSupport())) { + formatter = new ColorPatternFormatter(config.darken, config.format); + } else { + formatter = new PatternFormatter(config.format); + } } - errorManager = handler.getLocalErrorManager(); - return errorManager; + final ConsoleHandler consoleHandler = new ConsoleHandler(formatter); + consoleHandler.setLevel(config.level); + consoleHandler.setErrorManager(defaultErrorManager); + consoleHandler.setFilter(new LogCleanupFilter(filterElements)); + + final Handler handler = config.async.enable ? createAsyncHandler(config.async, config.level, consoleHandler) + : consoleHandler; + + if (formatterWarning) { + handler.getErrorManager().error("Multiple formatters were activated", null, ErrorManager.GENERIC_FAILURE); + } + + return handler; } - private void configureFileHandler(FileConfig config, ErrorManager errorManager, - List filterElements, ArrayList handlers) { + private static Handler configureFileHandler(final FileConfig config, final ErrorManager errorManager, + final List filterElements) { FileHandler handler = new FileHandler(); FileConfig.RotationConfig rotationConfig = config.rotation; if (rotationConfig.maxFileSize.isPresent() && rotationConfig.fileSuffix.isPresent()) { @@ -178,18 +272,14 @@ private void configureFileHandler(FileConfig config, ErrorManager errorManager, handler.setLevel(config.level); handler.setFilter(new LogCleanupFilter(filterElements)); if (config.async.enable) { - final AsyncHandler asyncHandler = new AsyncHandler(config.async.queueLength); - asyncHandler.setOverflowAction(config.async.overflow); - asyncHandler.addHandler(handler); - asyncHandler.setLevel(config.level); - handlers.add(asyncHandler); - } else { - handlers.add(handler); + return createAsyncHandler(config.async, config.level, handler); } + return handler; } - private void configureSyslogHandler(SyslogConfig config, ErrorManager errorManager, - List filterElements, ArrayList handlers) { + private static Handler configureSyslogHandler(final SyslogConfig config, + final ErrorManager errorManager, + final List filterElements) { try { final SyslogHandler handler = new SyslogHandler(config.endpoint.getHostString(), config.endpoint.getPort()); handler.setAppName(config.appName.orElse(getProcessName())); @@ -206,25 +296,21 @@ private void configureSyslogHandler(SyslogConfig config, ErrorManager errorManag handler.setErrorManager(errorManager); handler.setFilter(new LogCleanupFilter(filterElements)); if (config.async.enable) { - final AsyncHandler asyncHandler = new AsyncHandler(config.async.queueLength); - asyncHandler.setOverflowAction(config.async.overflow); - asyncHandler.addHandler(handler); - asyncHandler.setLevel(config.level); - handlers.add(asyncHandler); - } else { - handlers.add(handler); + return createAsyncHandler(config.async, config.level, handler); } + return handler; } catch (IOException e) { errorManager.error("Failed to create syslog handler", e, ErrorManager.OPEN_FAILURE); + return null; } } - public void initializeLoggingForImageBuild() { - if (ImageInfo.inImageBuildtimeCode()) { - final ConsoleHandler handler = new ConsoleHandler(new PatternFormatter( - "%d{HH:mm:ss,SSS} %-5p [%c{1.}] %s%e%n")); - handler.setLevel(Level.INFO); - InitialConfigurator.DELAYED_HANDLER.setHandlers(new Handler[] { handler }); - } + private static AsyncHandler createAsyncHandler(AsyncConfig asyncConfig, Level level, Handler handler) { + final AsyncHandler asyncHandler = new AsyncHandler(asyncConfig.queueLength); + asyncHandler.setOverflowAction(asyncConfig.overflow); + asyncHandler.addHandler(handler); + asyncHandler.setLevel(level); + return asyncHandler; } + } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/StringUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/StringUtil.java index 31b994c477cfa..25358cb1af1b4 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/StringUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/StringUtil.java @@ -1,6 +1,8 @@ package io.quarkus.runtime.util; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; import java.util.Objects; @@ -97,6 +99,13 @@ public String next() { }; } + /** + * @deprecated Use {@link String#join} instead. + * @param delim delimiter + * @param it iterator + * @return the joined string + */ + @Deprecated public static String join(String delim, Iterator it) { final StringBuilder b = new StringBuilder(); if (it.hasNext()) { @@ -167,6 +176,34 @@ public String next() { }; } + @SafeVarargs + public static List withoutSuffix(List list, T... segments) { + if (list.size() < segments.length) { + return list; + } + for (int i = 0; i < segments.length; i++) { + if (!list.get(list.size() - i - 1).equals(segments[segments.length - i - 1])) { + return list; + } + } + return list.subList(0, list.size() - segments.length); + } + + public static List toList(Iterator orig) { + return toList(orig, 0); + } + + private static List toList(Iterator orig, int idx) { + if (orig.hasNext()) { + final String item = orig.next(); + final List list = toList(orig, idx + 1); + list.set(idx, item); + return list; + } else { + return Arrays.asList(new String[idx]); + } + } + @SafeVarargs private static boolean arrayContains(final T item, final T... array) { for (T arrayItem : array) { diff --git a/core/runtime/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigFactory b/core/runtime/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigFactory new file mode 100644 index 0000000000000..0900f32a78ef9 --- /dev/null +++ b/core/runtime/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigFactory @@ -0,0 +1 @@ +io.quarkus.runtime.configuration.QuarkusConfigFactory diff --git a/core/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider b/core/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider deleted file mode 100644 index 778dff1e4a68c..0000000000000 --- a/core/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider +++ /dev/null @@ -1 +0,0 @@ -io.quarkus.runtime.configuration.TemporaryConfigSourceProvider diff --git a/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigExpanderTestCase.java b/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigExpanderTestCase.java index 0b3605e20e529..4c404b30c50f6 100644 --- a/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigExpanderTestCase.java +++ b/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigExpanderTestCase.java @@ -34,7 +34,11 @@ public static void initConfig() { @AfterEach public void doAfter() { - cpr.releaseConfig(config); + try { + cpr.releaseConfig(cpr.getConfig()); + } catch (IllegalStateException ignored) { + // just means no config was installed, which is fine + } } private SmallRyeConfig buildConfig(Map configMap) { diff --git a/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigProfileTestCase.java b/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigProfileTestCase.java index e09d72bb2fb0f..48b668ef8c06a 100644 --- a/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigProfileTestCase.java +++ b/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConfigProfileTestCase.java @@ -31,7 +31,11 @@ public static void initConfig() { @AfterEach public void doAfter() { - cpr.releaseConfig(config); + try { + cpr.releaseConfig(cpr.getConfig()); + } catch (IllegalStateException ignored) { + // just means no config was installed, which is fine + } } private SmallRyeConfig buildConfig(Map configMap) { diff --git a/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConverterSupportTest.java b/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConverterSupportTest.java deleted file mode 100644 index f4f7c29ea8a18..0000000000000 --- a/core/runtime/src/test/java/io/quarkus/runtime/configuration/ConverterSupportTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package io.quarkus.runtime.configuration; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.HashMap; -import java.util.Map; - -import javax.annotation.Priority; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigBuilder; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.Converter; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * A test cases against {@link ConverterSupport} class. - */ -public class ConverterSupportTest { - - public static class MyPojoUno { - } - - public static class MyPojoDuo { - } - - public static class MyPojoUnoConverter implements Converter { - - @Override - public MyPojoUno convert(final String value) { - return new MyPojoUno(); - } - } - - @Priority(333) - public static class MyPojoDuoConverter implements Converter { - - @Override - public MyPojoDuo convert(final String value) { - return new MyPojoDuo(); - } - } - - static class TestConverterWrapper { - - final Class type; - final int priority; - final Converter converter; - - public TestConverterWrapper(Class type, int priority, Converter converter) { - this.type = type; - this.priority = priority; - this.converter = converter; - } - } - - static class TestConfigBuilder implements ConfigBuilder { - - private final Map, TestConverterWrapper> wrappers = new HashMap<>(); - - @Override - public ConfigBuilder addDefaultSources() { - // do nothing - return null; - } - - @Override - public ConfigBuilder addDiscoveredSources() { - // do nothing - return null; - } - - @Override - public ConfigBuilder addDiscoveredConverters() { - // do nothing - return null; - } - - @Override - public ConfigBuilder forClassLoader(ClassLoader loader) { - // do nothing - return null; - } - - @Override - public ConfigBuilder withSources(ConfigSource... sources) { - // do nothing - return null; - } - - @Override - public ConfigBuilder withConverters(Converter... converters) { - // do nothing - return null; - } - - @Override - public ConfigBuilder withConverter(Class type, int priority, Converter converter) { - wrappers.put(type, new TestConverterWrapper(type, priority, converter)); - return null; - } - - @Override - public Config build() { - // do nothing - return null; - } - - public TestConverterWrapper get(Class type) { - return wrappers.get(type); - } - } - - TestConfigBuilder builder; - - @BeforeEach - public void setup() { - builder = new TestConfigBuilder(); - ConverterSupport.populateConverters(builder); - } - - @Test - public void testAllConvertersLoaded() { - assertNotNull(builder.get(MyPojoUno.class)); - assertNotNull(builder.get(MyPojoDuo.class)); - } - - @Test - public void testCorrectTypesMapping() { - assertSame(MyPojoUnoConverter.class, builder.get(MyPojoUno.class).converter.getClass()); - assertSame(MyPojoDuoConverter.class, builder.get(MyPojoDuo.class).converter.getClass()); - } - - @Test - public void testPriorityDefaults() { - assertTrue(builder.get(MyPojoUno.class).priority == 100); - } - - @Test - public void testPriorityFromAnnotation() { - assertTrue(builder.get(MyPojoDuo.class).priority == 333); - } -} diff --git a/core/runtime/src/test/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter b/core/runtime/src/test/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter deleted file mode 100644 index 89f86617564aa..0000000000000 --- a/core/runtime/src/test/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter +++ /dev/null @@ -1,2 +0,0 @@ -io.quarkus.runtime.configuration.ConverterSupportTest$MyPojoUnoConverter -io.quarkus.runtime.configuration.ConverterSupportTest$MyPojoDuoConverter diff --git a/core/test-extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java b/core/test-extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java index e972f4c79ae00..3b0d26a0df83f 100644 --- a/core/test-extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java +++ b/core/test-extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.BooleanSupplier; @@ -45,6 +46,7 @@ import io.quarkus.deployment.builditem.CapabilityBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; import io.quarkus.deployment.builditem.ObjectSubstitutionBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; @@ -64,6 +66,7 @@ import io.quarkus.extest.runtime.config.TestConfigRoot; import io.quarkus.extest.runtime.config.TestRunTimeConfig; import io.quarkus.extest.runtime.config.XmlConfig; +import io.quarkus.extest.runtime.logging.AdditionalLogHandlerValueFactory; import io.quarkus.extest.runtime.subst.DSAPublicKeyObjectSubstitution; import io.quarkus.extest.runtime.subst.KeyProxy; import io.quarkus.runtime.RuntimeValue; @@ -109,6 +112,17 @@ BeanDefiningAnnotationBuildItem registerBeanDefiningAnnotations() { return new BeanDefiningAnnotationBuildItem(TEST_ANNOTATION, TEST_ANNOTATION_SCOPE); } + /** + * Register an additional log handler + * + * @return LogHandlerBuildItem + */ + @BuildStep + @Record(RUNTIME_INIT) + LogHandlerBuildItem registerAdditionalLogHandler(final AdditionalLogHandlerValueFactory factory) { + return new LogHandlerBuildItem(factory.create()); + } + @BuildStep void registerNativeImageResources() { resource.produce(new NativeImageResourceBuildItem("/DSAPublicKey.encoded")); @@ -445,6 +459,16 @@ void registerFinalFieldReflectionObject(BuildProducer classes.produce(finalField); } + @BuildStep + void checkMapMap(TestBuildAndRunTimeConfig btrt, TestBuildTimeConfig bt, BuildProducer unused) { + if (!Objects.equals("1234", btrt.mapMap.get("outer-key").get("inner-key"))) { + throw new AssertionError("BTRT map map failed"); + } + if (!Objects.equals("1234", bt.mapMap.get("outer-key").get("inner-key"))) { + throw new AssertionError("BT map map failed"); + } + } + @BuildStep(onlyIf = Never.class) void neverRunThisOne() { throw new IllegalStateException("Not supposed to run!"); diff --git a/core/test-extension/deployment/src/main/resources/application-additional-handlers.properties b/core/test-extension/deployment/src/main/resources/application-additional-handlers.properties new file mode 100644 index 0000000000000..a84b1bf3b79de --- /dev/null +++ b/core/test-extension/deployment/src/main/resources/application-additional-handlers.properties @@ -0,0 +1,6 @@ +quarkus.log.level=INFO +quarkus.log.console.enable=true +quarkus.log.console.level=WARNING +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +# Resource path to DSAPublicKey base64 encoded bytes +quarkus.root.dsa-key-location=/DSAPublicKey.encoded diff --git a/core/test-extension/deployment/src/main/resources/application-category-configured-handlers-output.properties b/core/test-extension/deployment/src/main/resources/application-category-configured-handlers-output.properties new file mode 100644 index 0000000000000..c260065dd29ab --- /dev/null +++ b/core/test-extension/deployment/src/main/resources/application-category-configured-handlers-output.properties @@ -0,0 +1,20 @@ +quarkus.log.level=INFO +quarkus.log.console.enable=true +quarkus.log.console.level=WARNING +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +# Configure a named handler that logs to console +quarkus.log.handler.console."STRUCTURED_LOGGING".enable=true +quarkus.log.handler.console."STRUCTURED_LOGGING".format=%e%n +# Configure a named handler that logs to file +quarkus.log.handler.file."STRUCTURED_LOGGING_FILE".enable=true +quarkus.log.handler.file."STRUCTURED_LOGGING_FILE".format=%e%n +# Configure the category and link the two named handlers to it +quarkus.log.category."io.quarkus.category".level=INFO +quarkus.log.category."io.quarkus.category".handlers=STRUCTURED_LOGGING,STRUCTURED_LOGGING_FILE +# Configure other category and also link one named handler to it +quarkus.log.category."io.quarkus.othercategory".level=INFO +quarkus.log.category."io.quarkus.othercategory".handlers=STRUCTURED_LOGGING +# Configure other category and without linked named handlers +quarkus.log.category."io.quarkus.anothercategory".level=INFO +# Resource path to DSAPublicKey base64 encoded bytes +quarkus.root.dsa-key-location=/DSAPublicKey.encoded diff --git a/core/test-extension/deployment/src/main/resources/application-category-invalid-configured-handlers-output.properties b/core/test-extension/deployment/src/main/resources/application-category-invalid-configured-handlers-output.properties new file mode 100644 index 0000000000000..03c850103ebb4 --- /dev/null +++ b/core/test-extension/deployment/src/main/resources/application-category-invalid-configured-handlers-output.properties @@ -0,0 +1,15 @@ +quarkus.log.level=INFO +quarkus.log.console.enable=true +quarkus.log.console.level=WARNING +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +# Configure a named handler that logs to console +quarkus.log.handler.console."STRUCTURED_LOGGING".enable=true +quarkus.log.handler.console."STRUCTURED_LOGGING".format=%e%n +# Configure a named handler that logs to file but uses the same name which is not allowed +quarkus.log.handler.file."STRUCTURED_LOGGING".enable=true +quarkus.log.handler.file."STRUCTURED_LOGGING".format=%e%n +# Configure the category and link the two named handlers to it +quarkus.log.category."io.quarkus.category".level=INFO +quarkus.log.category."io.quarkus.category".handlers=STRUCTURED_LOGGING +# Resource path to DSAPublicKey base64 encoded bytes +quarkus.root.dsa-key-location=/DSAPublicKey.encoded diff --git a/core/test-extension/deployment/src/main/resources/application.properties b/core/test-extension/deployment/src/main/resources/application.properties index 894276bfffe01..4444b8a849388 100644 --- a/core/test-extension/deployment/src/main/resources/application.properties +++ b/core/test-extension/deployment/src/main/resources/application.properties @@ -48,6 +48,8 @@ quarkus.btrt.all-values.nested-config-map.key1.nested-value=value1 quarkus.btrt.all-values.nested-config-map.key1.oov=value1.1+value1.2 quarkus.btrt.all-values.nested-config-map.key2.nested-value=value2 quarkus.btrt.all-values.nested-config-map.key2.oov=value2.1+value2.2 +quarkus.btrt.all-values.string-list=value1,value2 +quarkus.btrt.all-values.long-list=1,2,3 ### Configuration settings for the TestRunTimeConfig config root quarkus.rt.rt-string-opt=rtStringOptValue @@ -97,9 +99,22 @@ quarkus.rt.one-to-nine=one,two,three,four,five,six,seven,eight,nine quarkus.rt.map-of-numbers.key1=one quarkus.rt.map-of-numbers.key2=two +### map configurations +quarkus.rt.leaf-map.key.first=first-key-value +quarkus.rt.leaf-map.key.second=second-key-value +quarkus.rt.config-group-map.key.group.nested-value=value +quarkus.rt.config-group-map.key.group.oov=value2.1+value2.2 ### build time and run time configuration using enhanced converters quarkus.btrt.map-of-numbers.key1=one quarkus.btrt.map-of-numbers.key2=two quarkus.btrt.my-enum=optional quarkus.btrt.my-enums=optional,enum-one,enum-two + +### anonymous root property +quarkus.test-property=foo + +### map of map of strings +quarkus.rt.map-map.outer-key.inner-key=1234 +quarkus.btrt.map-map.outer-key.inner-key=1234 +quarkus.bt.map-map.outer-key.inner-key=1234 diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/extest/ConfiguredBeanTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/extest/ConfiguredBeanTest.java index 64cbf269e3e60..c80903eb08742 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/extest/ConfiguredBeanTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/extest/ConfiguredBeanTest.java @@ -204,6 +204,20 @@ public void validateRuntimeConfigMap() { Assertions.assertEquals(Arrays.asList("value1", "value2", "value3"), stringListMap.get("key1")); Assertions.assertEquals(Arrays.asList("value4", "value5"), stringListMap.get("key2")); Assertions.assertEquals(Collections.singletonList("value6"), stringListMap.get("key3")); + + //quarkus.rt.leaf-map.key.first=first-key-value + //quarkus.rt.leaf-map.key.second=second-key-value + + final Map> leafMap = runTimeConfig.leafMap; + Assertions.assertEquals("first-key-value", leafMap.get("key").get("first")); + Assertions.assertEquals("second-key-value", leafMap.get("key").get("second")); + + //quarkus.rt.config-group-map.key.group.nested-value=value + //quarkus.rt.config-group-map.key.group.oov=value2.1+value2.2 + final Map> configGroupMap = runTimeConfig.configGroupMap; + NestedConfig nestedConfigFromMap = configGroupMap.get("key").get("group"); + Assertions.assertEquals("value", nestedConfigFromMap.nestedValue); + Assertions.assertEquals(new ObjectOfValue("value2.1", "value2.2"), nestedConfigFromMap.oov); } /** @@ -254,4 +268,26 @@ public void testConversionUsingConvertWith() { List actualMapValues = new ArrayList<>(configuredBean.getRunTimeConfig().mapOfNumbers.values()); Assertions.assertEquals(mapValues, actualMapValues); } + + @Test + public void testBtrtMapOfMap() { + Map> mapMap = configuredBean.getBuildTimeConfig().mapMap; + Assertions.assertFalse(mapMap.containsKey("inner-key")); + Assertions.assertTrue(mapMap.containsKey("outer-key")); + Map map = mapMap.get("outer-key"); + Assertions.assertTrue(map.containsKey("inner-key")); + Assertions.assertFalse(map.containsKey("outer-key")); + Assertions.assertEquals("1234", map.get("inner-key")); + } + + @Test + public void testRtMapOfMap() { + Map> mapMap = configuredBean.getRunTimeConfig().mapMap; + Assertions.assertFalse(mapMap.containsKey("inner-key")); + Assertions.assertTrue(mapMap.containsKey("outer-key")); + Map map = mapMap.get("outer-key"); + Assertions.assertTrue(map.containsKey("inner-key")); + Assertions.assertFalse(map.containsKey("outer-key")); + Assertions.assertEquals("1234", map.get("inner-key")); + } } diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/AdditionalHandlersTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AdditionalHandlersTest.java new file mode 100644 index 0000000000000..7f154ab1f567c --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AdditionalHandlersTest.java @@ -0,0 +1,34 @@ +package io.quarkus.logging; + +import static io.quarkus.logging.LoggingTestsHelper.getHandler; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.logging.Handler; +import java.util.logging.Level; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.extest.runtime.logging.AdditionalLogHandlerValueFactory.TestHandler; +import io.quarkus.test.QuarkusUnitTest; + +public class AdditionalHandlersTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-additional-handlers.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")); + + @Test + public void additionalHandlersConfigurationTest() { + Handler handler = getHandler(TestHandler.class); + assertThat(handler.getLevel()).isEqualTo(Level.FINE); + + TestHandler testHandler = (TestHandler) handler; + assertThat(testHandler.records).isNotEmpty(); + } + +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncConsoleHandlerTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncConsoleHandlerTest.java index 0c1c2543bb4fc..e49cfb1b5df3c 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncConsoleHandlerTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncConsoleHandlerTest.java @@ -1,20 +1,19 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.handlers.AsyncHandler; import org.jboss.logmanager.handlers.ConsoleHandler; -import org.jboss.logmanager.handlers.DelayedHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class AsyncConsoleHandlerTest { @@ -22,21 +21,13 @@ public class AsyncConsoleHandlerTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withConfigurationResource("application-async-console-log.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")) .setLogFileName("AsyncConsoleHandlerTest.log"); @Test public void asyncConsoleHandlerConfigurationTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()) - .filter(h -> (h instanceof AsyncHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(AsyncHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.WARNING); AsyncHandler asyncHandler = (AsyncHandler) handler; diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncFileHandlerTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncFileHandlerTest.java index 3a7694fab493c..f5bf133d2007f 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncFileHandlerTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncFileHandlerTest.java @@ -1,20 +1,19 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.handlers.AsyncHandler; -import org.jboss.logmanager.handlers.DelayedHandler; import org.jboss.logmanager.handlers.FileHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class AsyncFileHandlerTest { @@ -22,21 +21,13 @@ public class AsyncFileHandlerTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withConfigurationResource("application-async-file-log.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")) .setLogFileName("AsyncFileHandlerTest.log"); @Test public void asyncFileHandlerConfigurationTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()) - .filter(h -> (h instanceof AsyncHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(AsyncHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.INFO); AsyncHandler asyncHandler = (AsyncHandler) handler; diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncSyslogHandlerTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncSyslogHandlerTest.java index b59fe7f55349c..85e34a6d821f6 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncSyslogHandlerTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/AsyncSyslogHandlerTest.java @@ -1,20 +1,19 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.handlers.AsyncHandler; -import org.jboss.logmanager.handlers.DelayedHandler; import org.jboss.logmanager.handlers.SyslogHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class AsyncSyslogHandlerTest { @@ -22,21 +21,13 @@ public class AsyncSyslogHandlerTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withConfigurationResource("application-async-syslog.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")) .setLogFileName("AsyncSyslogHandlerTest.log"); @Test public void asyncSyslogHandlerConfigurationTest() throws NullPointerException { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()) - .filter(h -> (h instanceof AsyncHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(AsyncHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.WARNING); AsyncHandler asyncHandler = (AsyncHandler) handler; diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/CategoryConfiguredHandlerInvalidDueToMultipleHandlersTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/CategoryConfiguredHandlerInvalidDueToMultipleHandlersTest.java new file mode 100644 index 0000000000000..337ba7611d80f --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/CategoryConfiguredHandlerInvalidDueToMultipleHandlersTest.java @@ -0,0 +1,25 @@ +package io.quarkus.logging; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class CategoryConfiguredHandlerInvalidDueToMultipleHandlersTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setExpectedException(RuntimeException.class) + .withConfigurationResource("application-category-invalid-configured-handlers-output.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")); + + @Test + public void consoleOutputTest() { + Assertions.fail(); + } + +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/CategoryConfiguredHandlerTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/CategoryConfiguredHandlerTest.java new file mode 100644 index 0000000000000..873b17a21350c --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/CategoryConfiguredHandlerTest.java @@ -0,0 +1,61 @@ +package io.quarkus.logging; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.logging.*; + +import org.jboss.logmanager.formatters.PatternFormatter; +import org.jboss.logmanager.handlers.ConsoleHandler; +import org.jboss.logmanager.handlers.DelayedHandler; +import org.jboss.logmanager.handlers.FileHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.logging.InitialConfigurator; +import io.quarkus.test.QuarkusUnitTest; + +public class CategoryConfiguredHandlerTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-category-configured-handlers-output.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")); + + @Test + public void consoleOutputTest() { + LogManager logManager = LogManager.getLogManager(); + assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); + + DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; + assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); + + Handler handler = Arrays.stream(delayedHandler.getHandlers()).filter(h -> (h instanceof ConsoleHandler)) + .findFirst().get(); + assertThat(handler).isNotNull(); + assertThat(handler.getLevel()).isEqualTo(Level.WARNING); + + Logger categoryLogger = logManager.getLogger("io.quarkus.category"); + assertThat(categoryLogger).isNotNull(); + assertThat(categoryLogger.getHandlers()).hasSize(2).extracting("class").containsExactlyInAnyOrder(ConsoleHandler.class, + FileHandler.class); + + Logger otherCategoryLogger = logManager.getLogger("io.quarkus.othercategory"); + assertThat(otherCategoryLogger).isNotNull(); + assertThat(otherCategoryLogger.getHandlers()).hasSize(1).extracting("class") + .containsExactlyInAnyOrder(ConsoleHandler.class); + + Logger anotherCategoryLogger = logManager.getLogger("io.quarkus.anothercategory"); + assertThat(anotherCategoryLogger).isNotNull(); + assertThat(anotherCategoryLogger.getHandlers()).isEmpty(); + + Formatter formatter = handler.getFormatter(); + assertThat(formatter).isInstanceOf(PatternFormatter.class); + PatternFormatter patternFormatter = (PatternFormatter) formatter; + assertThat(patternFormatter.getPattern()).isEqualTo("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n"); + } + +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/ConsoleHandlerTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/ConsoleHandlerTest.java index 25d14b8aac735..72be77bf4c898 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/ConsoleHandlerTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/ConsoleHandlerTest.java @@ -1,39 +1,32 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.formatters.PatternFormatter; import org.jboss.logmanager.handlers.ConsoleHandler; -import org.jboss.logmanager.handlers.DelayedHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class ConsoleHandlerTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withConfigurationResource("application-console-output.properties"); + .withConfigurationResource("application-console-output.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")); @Test public void consoleOutputTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()).filter(h -> (h instanceof ConsoleHandler)) - .findFirst().get(); + Handler handler = getHandler(ConsoleHandler.class); assertThat(handler).isNotNull(); assertThat(handler.getLevel()).isEqualTo(Level.WARNING); diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/FileHandlerTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/FileHandlerTest.java index 417b7dd410704..4c97d6a9e9210 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/FileHandlerTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/FileHandlerTest.java @@ -1,40 +1,32 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.formatters.PatternFormatter; -import org.jboss.logmanager.handlers.DelayedHandler; import org.jboss.logmanager.handlers.FileHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class FileHandlerTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withConfigurationResource("application-file-output-log.properties"); + .withConfigurationResource("application-file-output-log.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")); @Test public void fileOutputTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()).filter(h -> (h instanceof FileHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(FileHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.INFO); Formatter formatter = handler.getFormatter(); diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/LoggingTestsHelper.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/LoggingTestsHelper.java new file mode 100644 index 0000000000000..1a6879fb096a6 --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/LoggingTestsHelper.java @@ -0,0 +1,30 @@ +package io.quarkus.logging; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import org.jboss.logmanager.handlers.DelayedHandler; + +import io.quarkus.runtime.logging.InitialConfigurator; + +public class LoggingTestsHelper { + + public static Handler getHandler(Class clazz) { + LogManager logManager = LogManager.getLogManager(); + assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); + + DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; + assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); + assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); + + Handler handler = Arrays.stream(delayedHandler.getHandlers()).filter(h -> (clazz.isInstance(h))) + .findFirst().get(); + assertThat(handler).isNotNull(); + return handler; + } +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicRotatingLoggingTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicRotatingLoggingTest.java index 657128a74f6fd..2a7e02df80762 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicRotatingLoggingTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicRotatingLoggingTest.java @@ -1,21 +1,19 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.formatters.PatternFormatter; -import org.jboss.logmanager.handlers.DelayedHandler; import org.jboss.logmanager.handlers.PeriodicRotatingFileHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class PeriodicRotatingLoggingTest { @@ -23,20 +21,13 @@ public class PeriodicRotatingLoggingTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withConfigurationResource("application-periodic-file-log-rotating.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")) .setLogFileName("PeriodicRotatingLoggingTest.log"); @Test public void periodicRotatingConfigurationTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()) - .filter(h -> (h instanceof PeriodicRotatingFileHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(PeriodicRotatingFileHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.INFO); Formatter formatter = handler.getFormatter(); diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicSizeRotatingLoggingTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicSizeRotatingLoggingTest.java index e0af0bd2d9e9a..ec2def70f9702 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicSizeRotatingLoggingTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/PeriodicSizeRotatingLoggingTest.java @@ -1,21 +1,19 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.formatters.PatternFormatter; -import org.jboss.logmanager.handlers.DelayedHandler; import org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class PeriodicSizeRotatingLoggingTest { @@ -23,20 +21,13 @@ public class PeriodicSizeRotatingLoggingTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withConfigurationResource("application-periodic-size-file-log-rotating.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")) .setLogFileName("PeriodicSizeRotatingLoggingTest.log"); @Test public void periodicSizeRotatingConfigurationTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()) - .filter(h -> (h instanceof PeriodicSizeRotatingFileHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(PeriodicSizeRotatingFileHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.INFO); Formatter formatter = handler.getFormatter(); diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/SizeRotatingLoggingTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/SizeRotatingLoggingTest.java index 1e187ebbecda7..4e9ff087f668d 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/SizeRotatingLoggingTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/SizeRotatingLoggingTest.java @@ -1,21 +1,19 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.formatters.PatternFormatter; -import org.jboss.logmanager.handlers.DelayedHandler; import org.jboss.logmanager.handlers.SizeRotatingFileHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class SizeRotatingLoggingTest { @@ -23,20 +21,13 @@ public class SizeRotatingLoggingTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withConfigurationResource("application-size-file-log-rotating.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")) .setLogFileName("SizeRotatingLoggingTest.log"); @Test public void sizeRotatingConfigurationTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()) - .filter(h -> (h instanceof SizeRotatingFileHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(SizeRotatingFileHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.INFO); Formatter formatter = handler.getFormatter(); diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/logging/SyslogHandlerTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/logging/SyslogHandlerTest.java index 9c4b64d97594c..6c6423feef649 100644 --- a/core/test-extension/deployment/src/test/java/io/quarkus/logging/SyslogHandlerTest.java +++ b/core/test-extension/deployment/src/test/java/io/quarkus/logging/SyslogHandlerTest.java @@ -1,42 +1,34 @@ package io.quarkus.logging; +import static io.quarkus.logging.LoggingTestsHelper.getHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.wildfly.common.net.HostName.getQualifiedHostName; import static org.wildfly.common.os.Process.getProcessName; -import java.util.Arrays; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; import org.jboss.logmanager.formatters.PatternFormatter; -import org.jboss.logmanager.handlers.DelayedHandler; import org.jboss.logmanager.handlers.SyslogHandler; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.runtime.logging.InitialConfigurator; import io.quarkus.test.QuarkusUnitTest; public class SyslogHandlerTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withConfigurationResource("application-syslog-output.properties"); + .withConfigurationResource("application-syslog-output.properties") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties")); @Test public void syslogOutputTest() { - LogManager logManager = LogManager.getLogManager(); - assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); - - DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; - assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); - - Handler handler = Arrays.stream(delayedHandler.getHandlers()).filter(h -> (h instanceof SyslogHandler)) - .findFirst().get(); - assertThat(handler).isNotNull(); + Handler handler = getHandler(SyslogHandler.class); assertThat(handler.getLevel()).isEqualTo(Level.WARNING); Formatter formatter = handler.getFormatter(); diff --git a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/MapMapConfig.java b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/MapMapConfig.java new file mode 100644 index 0000000000000..336c6ff0f37ed --- /dev/null +++ b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/MapMapConfig.java @@ -0,0 +1,17 @@ +package io.quarkus.extest.runtime.config; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * + */ +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED, name = "mm-root") +public class MapMapConfig { + //### map of map of strings + //quarkus.mm-root.map.inner-key.outer-key=1234 + + Map> map; +} diff --git a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildAndRunTimeConfig.java b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildAndRunTimeConfig.java index b44a89b14cc73..e9317f91b21d7 100644 --- a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildAndRunTimeConfig.java +++ b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildAndRunTimeConfig.java @@ -38,6 +38,8 @@ public class TestBuildAndRunTimeConfig { @ConvertWith(WholeNumberConverter.class) public Map mapOfNumbers; + public Map> mapMap; + /** * Enum object */ diff --git a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildTimeConfig.java b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildTimeConfig.java index 834193f4e0874..d8544999324e5 100644 --- a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildTimeConfig.java +++ b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestBuildTimeConfig.java @@ -1,5 +1,7 @@ package io.quarkus.extest.runtime.config; +import java.util.Map; + import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -25,6 +27,8 @@ public class TestBuildTimeConfig { @ConfigItem public AllValuesConfig allValues; + public Map> mapMap; + public TestBuildTimeConfig() { } diff --git a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestRunTimeConfig.java b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestRunTimeConfig.java index fe9ab9c45384a..ab963f4ec3000 100644 --- a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestRunTimeConfig.java +++ b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TestRunTimeConfig.java @@ -28,6 +28,13 @@ public class TestRunTimeConfig { @ConfigItem public AllValuesConfig allValues; + /** A map of properties */ + @ConfigItem + public Map> leafMap; + /** A map of property lists */ + @ConfigItem + public Map> configGroupMap; + /** * Enum object */ @@ -100,6 +107,8 @@ public class TestRunTimeConfig { @ConvertWith(WholeNumberConverter.class) public Map mapOfNumbers; + public Map> mapMap; + @Override public String toString() { return "TestRunTimeConfig{" + diff --git a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TopLevelRootConfig.java b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TopLevelRootConfig.java new file mode 100644 index 0000000000000..5e03ca0ecfac7 --- /dev/null +++ b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/config/TopLevelRootConfig.java @@ -0,0 +1,12 @@ +package io.quarkus.extest.runtime.config; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * + */ +@ConfigRoot(name = ConfigItem.PARENT) +public class TopLevelRootConfig { + String testProperty; +} diff --git a/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/logging/AdditionalLogHandlerValueFactory.java b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/logging/AdditionalLogHandlerValueFactory.java new file mode 100644 index 0000000000000..13d926b02dfba --- /dev/null +++ b/core/test-extension/runtime/src/main/java/io/quarkus/extest/runtime/logging/AdditionalLogHandlerValueFactory.java @@ -0,0 +1,43 @@ +package io.quarkus.extest.runtime.logging; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class AdditionalLogHandlerValueFactory { + + public RuntimeValue> create() { + return new RuntimeValue<>(Optional.of(new TestHandler())); + } + + public static class TestHandler extends Handler { + + public final List records = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() { + } + + @Override + public Level getLevel() { + return Level.FINE; + } + + @Override + public void close() throws SecurityException { + + } + } +} diff --git a/devtools/aesh/src/main/java/io/quarkus/cli/commands/AddExtensionCommand.java b/devtools/aesh/src/main/java/io/quarkus/cli/commands/AddExtensionCommand.java index 9845f41fdae58..351b1e1f88c84 100644 --- a/devtools/aesh/src/main/java/io/quarkus/cli/commands/AddExtensionCommand.java +++ b/devtools/aesh/src/main/java/io/quarkus/cli/commands/AddExtensionCommand.java @@ -1,8 +1,8 @@ package io.quarkus.cli.commands; import java.io.File; -import java.io.IOException; import java.util.Collections; +import java.util.List; import org.aesh.command.Command; import org.aesh.command.CommandDefinition; @@ -13,9 +13,9 @@ import org.aesh.command.option.Option; import org.aesh.io.Resource; +import io.quarkus.cli.commands.legacy.LegacyQuarkusCommandInvocation; import io.quarkus.cli.commands.writer.FileProjectWriter; import io.quarkus.dependencies.Extension; -import io.quarkus.maven.utilities.MojoUtils; /** * @author Ståle Pedersen @@ -38,19 +38,22 @@ public CommandResult execute(CommandInvocation commandInvocation) throws Command commandInvocation.println(commandInvocation.getHelpInfo("quarkus add-extension")); return CommandResult.SUCCESS; } else { - if (!findExtension(extension)) { + final QuarkusCommandInvocation quarkusInvocation = new LegacyQuarkusCommandInvocation(); + if (!findExtension(extension, quarkusInvocation.getPlatformDescriptor().getExtensions())) { commandInvocation.println("Can not find any extension named: " + extension); return CommandResult.SUCCESS; - } else if (pom.isLeaf()) { + } + if (pom.isLeaf()) { try { - File pomFile = new File(pom.getAbsolutePath()); + quarkusInvocation.setValue(AddExtensions.EXTENSIONS, Collections.singleton(extension)); + final File pomFile = new File(pom.getAbsolutePath()); AddExtensions project = new AddExtensions(new FileProjectWriter(pomFile.getParentFile())); - AddExtensionResult result = project.addExtensions(Collections.singleton(extension)); - if (!result.succeeded()) { + QuarkusCommandOutcome result = project.execute(quarkusInvocation); + if (!result.isSuccess()) { throw new CommandException("Unable to add an extension matching " + extension); } - } catch (IOException e) { - e.printStackTrace(); + } catch (Exception e) { + throw new CommandException("Unable to add an extension matching " + extension, e); } } @@ -59,10 +62,11 @@ public CommandResult execute(CommandInvocation commandInvocation) throws Command return CommandResult.SUCCESS; } - private boolean findExtension(String name) { - for (Extension ext : MojoUtils.loadExtensions()) { - if (ext.getName().equalsIgnoreCase(name)) + private boolean findExtension(String name, List extensions) { + for (Extension ext : extensions) { + if (ext.getName().equalsIgnoreCase(name)) { return true; + } } return false; } diff --git a/devtools/gradle/README.md b/devtools/gradle/README.md new file mode 100644 index 0000000000000..ece2cc9872c1b --- /dev/null +++ b/devtools/gradle/README.md @@ -0,0 +1,46 @@ +Quarkus Gradle Plugin +===================== + +Builds a Quarkus application, and provides helpers to launch dev-mode, the Quarkus CLI and the build of native images. + +Releases are published at https://plugins.gradle.org/plugin/io.quarkus . + +Functional Tests +---------------- + +To run the functional tests, run the following command: + +```bash +./gradlew functionalTests +``` + +Local development +----------------- + +1. Build the entire Quarkus codebase by running `mvn clean install -DskipTests -DskipITs` in the project root + - This should install the Gradle plugin in your local maven repository. + +2. Create a sample project using the Maven plugin: + +```bash + mvn io.quarkus:quarkus-maven-plugin:999-SNAPSHOT:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=my-gradle-project \ + -DclassName="org.acme.quickstart.GreetingResource" \ + -DplatformArtifactId=quarkus-bom \ + -Dpath="/hello" \ + -DbuildTool=gradle +``` + +Follow the instructions in the [Gradle Tooling Guide](https://quarkus.io/guides/gradle-tooling) for more information about the available commands. + +Importing using Intellij +------------------------- + +Disable "Maven Auto Import" for the Quarkus projects. Since the Gradle plugin has a pom.xml, +IntelliJ will configure this project as a Maven project. You need to configure it to be a Gradle +project. To do so, follow these instructions: + + +1. Go to File -> Project Structure +2. In Modules, remove the `quarkus-gradle-plugin` and re-import as a Gradle project. diff --git a/devtools/gradle/build.gradle b/devtools/gradle/build.gradle index 6e154ecd6edfb..3371351d5693c 100644 --- a/devtools/gradle/build.gradle +++ b/devtools/gradle/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'com.gradle.build-scan' version '2.3' id 'com.gradle.plugin-publish' version '0.10.1' id 'java-gradle-plugin' } @@ -9,27 +8,26 @@ if (JavaVersion.current().isJava9Compatible()) { compileJava.options.compilerArgs.addAll(['--release', '8']) } -compileJava { +compileJava { sourceCompatibility = '1.8' targetCompatibility = '1.8' } repositories { - maven { url 'target/dependencies/' } + mavenLocal() mavenCentral() - maven { url 'https://repo.gradle.org/gradle/libs-releases-local/' } } dependencies { - compile "io.quarkus:quarkus-bootstrap-core:${version}" - compile "io.quarkus:quarkus-devtools-common:${version}" - compile "io.quarkus:quarkus-platform-descriptor-json:${version}" - compile "io.quarkus:quarkus-platform-descriptor-resolver-json:${version}" - compile "io.quarkus:quarkus-development-mode:${version}" - compile "io.quarkus:quarkus-creator:${version}" - compile gradleApi() + api gradleApi() + implementation "io.quarkus:quarkus-bootstrap-core:${version}" + implementation "io.quarkus:quarkus-devtools-common:${version}" + implementation "io.quarkus:quarkus-platform-descriptor-json:${version}" + implementation "io.quarkus:quarkus-platform-descriptor-resolver-json:${version}" + implementation "io.quarkus:quarkus-development-mode:${version}" + implementation "io.quarkus:quarkus-creator:${version}" - testImplementation 'org.assertj:assertj-core:3.13.2' + testImplementation 'org.assertj:assertj-core:3.14.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.5.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2' @@ -63,8 +61,26 @@ gradlePlugin { } } -buildScan { - //See also: https://docs.gradle.com/build-scan-plugin/ - termsOfServiceUrl = 'https://gradle.com/terms-of-service'; - termsOfServiceAgree = 'yes' +// Add a source set for the functional test suite +sourceSets { + functionalTest { + } +} + +gradlePlugin.testSourceSets(sourceSets.functionalTest) +configurations.functionalTestImplementation.extendsFrom(configurations.testImplementation) +configurations.functionalTestRuntime.extendsFrom(configurations.testRuntimeOnly) + +// Add a task to run the functional tests +task functionalTest(type: Test) { + description = "Runs functional tests" + group = "verification" + useJUnitPlatform() + testClassesDirs = sourceSets.functionalTest.output.classesDirs + classpath = sourceSets.functionalTest.runtimeClasspath } + +check { + // Run the functional tests as part of `check` + dependsOn(tasks.functionalTest) +} \ No newline at end of file diff --git a/devtools/gradle/gradle/wrapper/gradle-wrapper.jar b/devtools/gradle/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b38..cc4fdc293d0e5 100644 Binary files a/devtools/gradle/gradle/wrapper/gradle-wrapper.jar and b/devtools/gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/devtools/gradle/gradle/wrapper/gradle-wrapper.properties b/devtools/gradle/gradle/wrapper/gradle-wrapper.properties index b1e3327d7aeba..0a13f4952d343 100644 --- a/devtools/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/devtools/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -#Tue Nov 05 11:34:08 BRST 2019 -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +#Tue Nov 26 00:14:48 BRST 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/devtools/gradle/gradlew b/devtools/gradle/gradlew index 83f2acfdc319a..2fe81a7d95e4f 100755 --- a/devtools/gradle/gradlew +++ b/devtools/gradle/gradlew @@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/devtools/gradle/pom.xml b/devtools/gradle/pom.xml index 0e6b7630d1ced..e7ec22027c3b6 100644 --- a/devtools/gradle/pom.xml +++ b/devtools/gradle/pom.xml @@ -10,7 +10,7 @@ 4.0.0 - quarkus-gradle-plugin + io.quarkus.gradle.plugin pom Quarkus - Gradle Plugin Quarkus - Gradle Plugin @@ -21,30 +21,7 @@ false - - - gradle-official-repository - Gradle Official Repository - https://repo.gradle.org/gradle/libs-releases-local/ - - - - - - - io.quarkus - quarkus-bom - ${project.version} - pom - - - io.quarkus - quarkus-bom-deployment - ${project.version} - pom - - io.quarkus @@ -70,47 +47,9 @@ io.quarkus quarkus-platform-descriptor-resolver-json - - - org.apache.maven - maven-model - - - - - org.junit.jupiter - junit-jupiter - test - - - org.assertj - assertj-core - test - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - generate-resources - - copy-dependencies - - false - - true - true - true - true - ${project.build.directory}/dependencies - - - - org.codehaus.mojo exec-maven-plugin @@ -125,9 +64,11 @@ ${gradle.task} -Pdescription=${project.description} -S + --stacktrace ${settings.localRepository} + ${env.MAVEN_OPTS} ${skip.gradle.build} @@ -137,51 +78,6 @@ - - maven-resources-plugin - - - copy-gradle-jars - package - - copy-resources - - - - ${basedir}/target - - - build/libs/ - - ${project.artifactId}-${project.version}.jar - ${project.artifactId}-${project.version}-javadoc.jar - ${project.artifactId}-${project.version}-sources.jar - - - - - - - - - maven-assembly-plugin - - - src/assembly/copy.xml - - false - false - - - - create-repo - package - - single - - - - org.codehaus.mojo build-helper-maven-plugin @@ -195,16 +91,16 @@ - target/${project.artifactId}-${project.version}.jar + build/libs/quarkus-gradle-plugin-${project.version}.jar jar - target/${project.artifactId}-${project.version}-javadoc.jar + build/libs/quarkus-gradle-plugin-${project.version}-javadoc.jar jar javadoc - target/${project.artifactId}-${project.version}-sources.jar + build/libs/quarkus-gradle-plugin-${project.version}-sources.jar jar sources @@ -214,6 +110,15 @@ + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + true + + @@ -240,39 +145,5 @@ assemble - - full - - - full - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - attach-zip - - attach-artifact - - - - - ${project.build.directory}/${project.artifactId}-${project.version}-docs.zip - zip - docs - - - ${skip.gradle.build} - - - - - - - diff --git a/devtools/gradle/settings.gradle b/devtools/gradle/settings.gradle index 92bb81329cd94..4de2a058ed852 100644 --- a/devtools/gradle/settings.gradle +++ b/devtools/gradle/settings.gradle @@ -1 +1,15 @@ +plugins { + id "com.gradle.enterprise" version "3.1" +} + +gradleEnterprise { + buildScan { + // plugin configuration + //See also: https://docs.gradle.com/enterprise/gradle-plugin/ + termsOfServiceUrl = 'https://gradle.com/terms-of-service'; + termsOfServiceAgree = 'yes' + publishOnFailure() + } +} + rootProject.name = 'quarkus-gradle-plugin' diff --git a/devtools/gradle/src/assembly/copy.xml b/devtools/gradle/src/assembly/copy.xml deleted file mode 100644 index d2261e83989cf..0000000000000 --- a/devtools/gradle/src/assembly/copy.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - repo - - dir - - - - ${basedir}/build/libs/${project.artifactId}-${project.version}.jar - io/quarkus/quarkus-gradle-plugin/${project.version}/ - - - pom.xml - io/quarkus/quarkus-gradle-plugin/${project.version}/ - quarkus-gradle-plugin-${project.version}.pom - - - \ No newline at end of file diff --git a/devtools/gradle/src/functionalTest/java/io/quarkus/gradle/QuarkusPluginFunctionalTest.java b/devtools/gradle/src/functionalTest/java/io/quarkus/gradle/QuarkusPluginFunctionalTest.java new file mode 100644 index 0000000000000..c43693b5d0289 --- /dev/null +++ b/devtools/gradle/src/functionalTest/java/io/quarkus/gradle/QuarkusPluginFunctionalTest.java @@ -0,0 +1,90 @@ +package io.quarkus.gradle; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.cli.commands.CreateProject; +import io.quarkus.cli.commands.writer.FileProjectWriter; +import io.quarkus.generators.BuildTool; +import io.quarkus.generators.SourceType; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QuarkusPluginFunctionalTest { + + private File projectRoot; + + @BeforeEach + void setUp(@TempDir File projectRoot) { + this.projectRoot = projectRoot; + } + + @Test + public void canRunListExtensions() throws IOException { + createProject(SourceType.JAVA); + + BuildResult build = GradleRunner.create() + .forwardOutput() + .withPluginClasspath() + .withArguments(arguments("listExtensions")) + .withProjectDir(projectRoot) + .build(); + + assertThat(build.task(":listExtensions").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(build.getOutput()).contains("Quarkus - Core"); + } + + @ParameterizedTest(name = "Build {0} project") + @EnumSource(SourceType.class) + public void canBuild(SourceType sourceType) throws IOException { + createProject(sourceType); + + BuildResult build = GradleRunner.create() + .forwardOutput() + .withPluginClasspath() + .withArguments(arguments("build")) + .withProjectDir(projectRoot) + .build(); + + assertThat(build.task(":build").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + // gradle build should not build the native image + assertThat(build.task(":buildNative")).isNull(); + } + + private List arguments(String argument) { + List arguments = new ArrayList<>(); + arguments.add(argument); + String mavenRepoLocal = System.getProperty("maven.repo.local", System.getenv("MAVEN_LOCAL_REPO")); + if (mavenRepoLocal != null) { + arguments.add("-Dmaven.repo.local=" + mavenRepoLocal); + } + return arguments; + } + + private void createProject(SourceType sourceType) throws IOException { + Map context = new HashMap<>(); + context.put("path", "/greeting"); + assertThat(new CreateProject(new FileProjectWriter(projectRoot)) + .groupId("com.acme.foo") + .artifactId("foo") + .version("1.0.0-SNAPSHOT") + .buildTool(BuildTool.GRADLE) + .className("org.acme.GreetingResource") + .sourceType(sourceType) + .doCreateProject(context)) + .withFailMessage("Project was not created") + .isTrue(); + } +} \ No newline at end of file diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java b/devtools/gradle/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java index dfce575967b4c..16862ef104244 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java @@ -25,6 +25,9 @@ import org.gradle.api.artifacts.ResolvedConfiguration; import org.gradle.api.file.RegularFile; import org.gradle.api.internal.artifacts.DefaultModuleIdentifier; +import org.gradle.api.internal.artifacts.dependencies.DefaultDependencyArtifact; +import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.Provider; import org.gradle.jvm.tasks.Jar; @@ -38,6 +41,7 @@ public class AppModelGradleResolver implements AppModelResolver { private AppModel appModel; + private final Project project; public AppModelGradleResolver(Project project) { @@ -45,7 +49,8 @@ public AppModelGradleResolver(Project project) { } @Override - public String getLatestVersion(AppArtifact arg0, String arg1, boolean arg2) throws AppModelResolverException { + public String getLatestVersion(AppArtifact appArtifact, String upToVersion, boolean inclusive) + throws AppModelResolverException { throw new UnsupportedOperationException(); } @@ -55,18 +60,20 @@ public String getLatestVersionFromRange(AppArtifact appArtifact, String range) t } @Override - public String getNextVersion(AppArtifact arg0, String fromVersion, boolean fromVersionIncluded, String arg1, boolean arg2) + public String getNextVersion(AppArtifact appArtifact, String fromVersion, boolean fromVersionIncluded, String upToVersion, + boolean upToVersionIncluded) throws AppModelResolverException { throw new UnsupportedOperationException(); } @Override - public List listLaterVersions(AppArtifact arg0, String arg1, boolean arg2) throws AppModelResolverException { + public List listLaterVersions(AppArtifact appArtifact, String upToVersion, boolean inclusive) + throws AppModelResolverException { throw new UnsupportedOperationException(); } @Override - public void relink(AppArtifact arg0, Path arg1) throws AppModelResolverException { + public void relink(AppArtifact appArtifact, Path localPath) throws AppModelResolverException { } @@ -74,12 +81,12 @@ public void relink(AppArtifact arg0, Path arg1) throws AppModelResolverException public Path resolve(AppArtifact appArtifact) throws AppModelResolverException { if (!appArtifact.isResolved()) { - final GradleDependencyArtifact dep = new GradleDependencyArtifact(); + final DefaultDependencyArtifact dep = new DefaultDependencyArtifact(); dep.setExtension(appArtifact.getType()); dep.setType(appArtifact.getType()); dep.setName(appArtifact.getArtifactId()); - final QuarkusExtDependency gradleDep = new QuarkusExtDependency(appArtifact.getGroupId(), + final DefaultExternalModuleDependency gradleDep = new DefaultExternalModuleDependency(appArtifact.getGroupId(), appArtifact.getArtifactId(), appArtifact.getVersion(), null); gradleDep.addArtifact(dep); @@ -103,8 +110,7 @@ public Path resolve(AppArtifact appArtifact) throws AppModelResolverException { } @Override - public List resolveUserDependencies(AppArtifact appArtifact, List directDeps) - throws AppModelResolverException { + public List resolveUserDependencies(AppArtifact appArtifact, List directDeps) { return Collections.emptyList(); } @@ -113,7 +119,7 @@ public AppModel resolveModel(AppArtifact appArtifact) throws AppModelResolverExc if (appModel != null && appModel.getAppArtifact().equals(appArtifact)) { return appModel; } - final Configuration compileCp = project.getConfigurations().getByName("compileClasspath"); + final Configuration compileCp = project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME); final List extensionDeps = new ArrayList<>(); final List userDeps = new ArrayList<>(); Map userModules = new HashMap<>(); @@ -158,7 +164,7 @@ public AppModel resolveModel(AppArtifact appArtifact) throws AppModelResolverExc // In the case of quarkusBuild (which is the primary user of this), // it's not necessary to actually resolve the original application JAR if (!appArtifact.isResolved()) { - final Jar jarTask = (Jar) project.getTasks().findByName("jar"); + final Jar jarTask = (Jar) project.getTasks().findByName(JavaPlugin.JAR_TASK_NAME); if (jarTask == null) { throw new AppModelResolverException("Failed to locate task 'jar' in the project."); } @@ -174,7 +180,7 @@ public AppModel resolveModel(AppArtifact appArtifact) throws AppModelResolverExc } @Override - public AppModel resolveModel(AppArtifact arg0, List arg1) throws AppModelResolverException { + public AppModel resolveModel(AppArtifact root, List deps) throws AppModelResolverException { throw new UnsupportedOperationException(); } @@ -205,7 +211,7 @@ private Dependency processQuarkusDir(ResolvedArtifact a, Path quarkusDir) { String value = extProps.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); final String[] split = value.split(":"); - return new QuarkusExtDependency(split[0], split[1], split[2], null); + return new DefaultExternalModuleDependency(split[0], split[1], split[2], null); } private Properties resolveDescriptor(final Path path) { @@ -222,4 +228,4 @@ private Properties resolveDescriptor(final Path path) { } return rtProps; } -} \ No newline at end of file +} diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/GradleDependencyArtifact.java b/devtools/gradle/src/main/java/io/quarkus/gradle/GradleDependencyArtifact.java deleted file mode 100644 index 2c3ad51f02b3f..0000000000000 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/GradleDependencyArtifact.java +++ /dev/null @@ -1,117 +0,0 @@ -package io.quarkus.gradle; - -import org.gradle.api.artifacts.DependencyArtifact; - -public class GradleDependencyArtifact implements DependencyArtifact { - - private String classifier; - private String extension; - private String name; - private String type; - private String url; - - @Override - public String getClassifier() { - return classifier; - } - - @Override - public String getExtension() { - return extension; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getType() { - return type; - } - - @Override - public String getUrl() { - return url; - } - - @Override - public void setClassifier(String arg0) { - this.classifier = arg0; - } - - @Override - public void setExtension(String arg0) { - this.extension = arg0; - } - - @Override - public void setName(String arg0) { - this.name = arg0; - } - - @Override - public void setType(String arg0) { - this.type = arg0; - } - - @Override - public void setUrl(String arg0) { - this.url = arg0; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((classifier == null) ? 0 : classifier.hashCode()); - result = prime * result + ((extension == null) ? 0 : extension.hashCode()); - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((type == null) ? 0 : type.hashCode()); - result = prime * result + ((url == null) ? 0 : url.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - GradleDependencyArtifact other = (GradleDependencyArtifact) obj; - if (classifier == null) { - if (other.classifier != null) - return false; - } else if (!classifier.equals(other.classifier)) - return false; - if (extension == null) { - if (other.extension != null) - return false; - } else if (!extension.equals(other.extension)) - return false; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (type == null) { - if (other.type != null) - return false; - } else if (!type.equals(other.type)) - return false; - if (url == null) { - if (other.url != null) - return false; - } else if (!url.equals(other.url)) - return false; - return true; - } - - @Override - public String toString() { - return "GradleDepArtifact [classifier=" + classifier + ", extension=" + extension + ", name=" + name + ", type=" + type - + ", url=" + url + "]"; - } -} diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusExtDependency.java b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusExtDependency.java deleted file mode 100644 index 48c10ab328fa5..0000000000000 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusExtDependency.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.quarkus.gradle; - -import java.util.Set; - -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.artifacts.DependencyArtifact; -import org.gradle.api.artifacts.ExternalModuleDependency; -import org.gradle.api.internal.artifacts.DefaultModuleIdentifier; -import org.gradle.api.internal.artifacts.dependencies.AbstractExternalModuleDependency; - -public class QuarkusExtDependency extends AbstractExternalModuleDependency { - - private final String group; - private final String name; - private final String version; - private final String configuration; - - public QuarkusExtDependency(String group, String name, String version, String configuration) { - super(DefaultModuleIdentifier.newId(group, name), version, configuration); - this.group = group; - this.name = name; - this.version = version; - this.configuration = configuration; - } - - @Override - public Set getArtifacts() { - return super.getArtifacts(); - } - - @Override - public ExternalModuleDependency copy() { - QuarkusExtDependency copy = new QuarkusExtDependency(group, name, version, configuration); - final Set artifacts = getArtifacts(); - for (DependencyArtifact a : artifacts) { - copy.addArtifact(a); - } - return copy; - } - - @Override - public boolean contentEquals(Dependency arg0) { - new Exception("contentEquals " + arg0).printStackTrace(); - return true; - } -} diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 66ca0a091f4be..ff82454012279 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -12,7 +12,6 @@ import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.SourceSetOutput; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.testing.Test; import org.gradle.util.GradleVersion; @@ -26,28 +25,45 @@ import io.quarkus.gradle.tasks.QuarkusTestConfig; import io.quarkus.gradle.tasks.QuarkusTestNative; -/** - * @author Ståle Pedersen - */ public class QuarkusPlugin implements Plugin { + public static final String ID = "io.quarkus"; + + public static final String EXTENSION_NAME = "quarkus"; + public static final String LIST_EXTENSIONS_TASK_NAME = "listExtensions"; + public static final String ADD_EXTENSION_TASK_NAME = "addExtension"; + public static final String QUARKUS_BUILD_TASK_NAME = "quarkusBuild"; + public static final String GENERATE_CONFIG_TASK_NAME = "generateConfig"; + public static final String QUARKUS_DEV_TASK_NAME = "quarkusDev"; + public static final String BUILD_NATIVE_TASK_NAME = "buildNative"; + public static final String TEST_NATIVE_TASK_NAME = "testNative"; + public static final String QUARKUS_TEST_CONFIG_TASK_NAME = "quarkusTestConfig"; + + // this name has to be the same as the directory in which the tests reside + public static final String NATIVE_TEST_SOURCE_SET_NAME = "native-test"; + + public static final String NATIVE_TEST_IMPLEMENTATION_CONFIGURATION_NAME = "nativeTestImplementation"; + public static final String NATIVE_TEST_RUNTIME_ONLY_CONFIGURATION_NAME = "nativeTestRuntimeOnly"; + @Override public void apply(Project project) { verifyGradleVersion(); // register extension - project.getExtensions().create("quarkus", QuarkusPluginExtension.class, project); + project.getExtensions().create(EXTENSION_NAME, QuarkusPluginExtension.class, project); registerTasks(project); } private void registerTasks(Project project) { TaskContainer tasks = project.getTasks(); - tasks.create("listExtensions", QuarkusListExtensions.class); - tasks.create("addExtension", QuarkusAddExtension.class); + tasks.create(LIST_EXTENSIONS_TASK_NAME, QuarkusListExtensions.class); + tasks.create(ADD_EXTENSION_TASK_NAME, QuarkusAddExtension.class); + tasks.create(GENERATE_CONFIG_TASK_NAME, QuarkusGenerateConfig.class); - Task quarkusBuild = tasks.create("quarkusBuild", QuarkusBuild.class); - Task quarkusGenerateConfig = tasks.create("generateConfig", QuarkusGenerateConfig.class); - Task quarkusDev = tasks.create("quarkusDev", QuarkusDev.class); + Task quarkusBuild = tasks.create(QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class); + Task quarkusDev = tasks.create(QUARKUS_DEV_TASK_NAME, QuarkusDev.class); + Task buildNative = tasks.create(BUILD_NATIVE_TASK_NAME, QuarkusNative.class); + Task quarkusTestConfig = tasks.create(QUARKUS_TEST_CONFIG_TASK_NAME, QuarkusTestConfig.class); project.getPlugins().withType( BasePlugin.class, @@ -57,46 +73,43 @@ private void registerTasks(Project project) { javaPlugin -> { Task classesTask = tasks.getByName(JavaPlugin.CLASSES_TASK_NAME); quarkusDev.dependsOn(classesTask); - quarkusBuild.dependsOn(classesTask); - - Task jarTask = tasks.getByName(JavaPlugin.JAR_TASK_NAME); - quarkusBuild.dependsOn(jarTask); + quarkusBuild.dependsOn(classesTask, tasks.getByName(JavaPlugin.JAR_TASK_NAME)); + + buildNative.dependsOn(tasks.getByName(BasePlugin.ASSEMBLE_TASK_NAME)); + + SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class) + .getSourceSets(); + SourceSet nativeTestSourceSet = sourceSets.create(NATIVE_TEST_SOURCE_SET_NAME); + SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + SourceSet testSourceSet = sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME); + + nativeTestSourceSet.setCompileClasspath( + nativeTestSourceSet.getCompileClasspath() + .plus(mainSourceSet.getOutput()) + .plus(testSourceSet.getOutput())); + + nativeTestSourceSet.setRuntimeClasspath( + nativeTestSourceSet.getRuntimeClasspath() + .plus(mainSourceSet.getOutput()) + .plus(testSourceSet.getOutput())); + + // create a custom configuration to be used for the dependencies of the testNative task + ConfigurationContainer configurations = project.getConfigurations(); + configurations.maybeCreate(NATIVE_TEST_IMPLEMENTATION_CONFIGURATION_NAME) + .extendsFrom(configurations.findByName(JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME)); + configurations.maybeCreate(NATIVE_TEST_RUNTIME_ONLY_CONFIGURATION_NAME) + .extendsFrom(configurations.findByName(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME)); + + Task testNative = tasks.create(TEST_NATIVE_TASK_NAME, QuarkusTestNative.class); + testNative.dependsOn(buildNative); + testNative.setShouldRunAfter(Collections.singletonList(tasks.findByName(JavaPlugin.TEST_TASK_NAME))); + tasks.withType(Test.class).forEach(t -> { + // Quarkus test configuration task which should be executed before any Quarkus test + t.dependsOn(quarkusTestConfig); + // also make each task use the JUnit platform since it's the only supported test environment + t.useJUnitPlatform(); + }); }); - - Task buildNative = tasks.create("buildNative", QuarkusNative.class); - - // set up the source set for the testNative - JavaPluginConvention javaPlugin = project.getConvention().findPlugin(JavaPluginConvention.class); - if (javaPlugin != null) { - buildNative.dependsOn(tasks.getByName(BasePlugin.ASSEMBLE_TASK_NAME)); - - SourceSetContainer sourceSets = javaPlugin.getSourceSets(); - SourceSet nativeTestSourceSet = sourceSets.create("native-test"); // this name has to be the same as the directory in which the tests reside - SourceSetOutput mainSourceSetOutput = sourceSets.getByName("main").getOutput(); - SourceSetOutput testSourceSetOutput = sourceSets.getByName("test").getOutput(); - nativeTestSourceSet.setCompileClasspath( - nativeTestSourceSet.getCompileClasspath().plus(mainSourceSetOutput).plus(testSourceSetOutput)); - nativeTestSourceSet.setRuntimeClasspath( - nativeTestSourceSet.getRuntimeClasspath().plus(mainSourceSetOutput).plus(testSourceSetOutput)); - - // create a custom configuration to be used for the dependencies of the testNative task - ConfigurationContainer configurations = project.getConfigurations(); - configurations.maybeCreate("nativeTestImplementation").extendsFrom(configurations.findByName("implementation")); - configurations.maybeCreate("nativeTestRuntimeOnly").extendsFrom(configurations.findByName("runtimeOnly")); - - Task testNative = tasks.create("testNative", QuarkusTestNative.class).dependsOn(buildNative); - testNative.setShouldRunAfter(Collections.singletonList(tasks.findByName("test"))); - - tasks.getByName("check").dependsOn(testNative); - } - - final QuarkusTestConfig quarkusTestConfig = tasks.create("quarkusTestConfig", QuarkusTestConfig.class); - tasks.withType(Test.class).forEach(t -> { - // Quarkus test configuration task which should be executed before any Quarkus test - t.dependsOn(quarkusTestConfig); - // also make each task use the JUnit platform since it's the only supported test environment - t.useJUnitPlatform(); - }); } private void verifyGradleVersion() { diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPluginExtension.java b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPluginExtension.java index 2232888290986..d48acd6ec7db2 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPluginExtension.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPluginExtension.java @@ -4,74 +4,92 @@ import java.util.Set; import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; import io.quarkus.bootstrap.model.AppArtifact; import io.quarkus.bootstrap.resolver.AppModelResolver; -/** - * @author Ståle Pedersen - */ public class QuarkusPluginExtension { private final Project project; - private String outputDirectory; + private File outputDirectory; private String finalName; - private String sourceDir; + private File sourceDir; - private String workingDir; + private File workingDir; - private String outputConfigDirectory; + private File outputConfigDirectory; public QuarkusPluginExtension(Project project) { this.project = project; } public File outputDirectory() { - if (outputDirectory == null) - outputDirectory = project.getConvention().getPlugin(JavaPluginConvention.class) - .getSourceSets().getByName("main").getOutput().getClassesDirs().getAsPath(); + if (outputDirectory == null) { + outputDirectory = getLastFile(project.getConvention().getPlugin(JavaPluginConvention.class) + .getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs()); + } + return outputDirectory; + } - return new File(outputDirectory); + public void setOutputDirectory(String outputDirectory) { + this.outputDirectory = new File(outputDirectory); } public File outputConfigDirectory() { if (outputConfigDirectory == null) { outputConfigDirectory = project.getConvention().getPlugin(JavaPluginConvention.class) - .getSourceSets().getByName("main").getOutput().getResourcesDir().getAbsolutePath(); + .getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getResourcesDir(); } - return new File(outputConfigDirectory); + return outputConfigDirectory; + } + + public void setOutputConfigDirectory(String outputConfigDirectory) { + this.outputConfigDirectory = new File(outputConfigDirectory); } public File sourceDir() { if (sourceDir == null) { - sourceDir = project.getConvention().getPlugin(JavaPluginConvention.class) - .getSourceSets().getByName("main").getAllJava().getSourceDirectories().getAsPath(); + sourceDir = getLastFile(project.getConvention().getPlugin(JavaPluginConvention.class) + .getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getAllJava().getSourceDirectories()); } - return new File(sourceDir); + return sourceDir; + } + + public void setSourceDir(String sourceDir) { + this.sourceDir = new File(sourceDir); } public File workingDir() { if (workingDir == null) { - workingDir = outputDirectory().getPath(); + workingDir = outputDirectory(); } + return workingDir; + } - return new File(workingDir); + public void setWorkingDir(String workingDir) { + this.workingDir = new File(workingDir); } public String finalName() { if (finalName == null || finalName.length() == 0) { - this.finalName = project.getName() + "-" + project.getVersion(); + this.finalName = String.format("%s-%s", project.getName(), project.getVersion()); } return finalName; } + public void setFinalName(String finalName) { + this.finalName = finalName; + } + public Set resourcesDir() { return project.getConvention().getPlugin(JavaPluginConvention.class) - .getSourceSets().getByName("main").getResources().getSrcDirs(); + .getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getResources().getSrcDirs(); } public AppArtifact getAppArtifact() { @@ -82,4 +100,18 @@ public AppArtifact getAppArtifact() { public AppModelResolver resolveAppModel() { return new AppModelGradleResolver(project); } + + /** + * Returns the last file from the specified {@link FileCollection}. + * Needed for the Scala plugin. + */ + private File getLastFile(FileCollection fileCollection) { + File result = null; + for (File f : fileCollection) { + if (result == null || f.exists()) { + result = f; + } + } + return result; + } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleLogger.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleLogger.java index d8d4037400bc7..54404a76c42e8 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleLogger.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleLogger.java @@ -9,9 +9,6 @@ import org.jboss.logging.LoggerProvider; import org.wildfly.common.Assert; -/** - * @author Ståle Pedersen - */ public class GradleLogger implements LoggerProvider { static final Object[] NO_PARAMS = new Object[0]; diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleMessageWriter.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleMessageWriter.java index a91641293e540..00deba251f42e 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleMessageWriter.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/GradleMessageWriter.java @@ -13,18 +13,18 @@ public GradleMessageWriter(Logger logger) { } @Override - public void debug(String arg0) { - logger.debug(arg0); + public void debug(String msg) { + logger.debug(msg); } @Override - public void error(String arg0) { - logger.error(arg0); + public void error(String msg) { + logger.error(msg); } @Override - public void info(String arg0) { - logger.info(arg0); + public void info(String msg) { + logger.info(msg); } @Override @@ -33,7 +33,7 @@ public boolean isDebugEnabled() { } @Override - public void warn(String arg0) { - logger.warn(arg0); + public void warn(String msg) { + logger.warn(msg); } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java index c820723b5d2aa..72eefe1294f4a 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java @@ -3,7 +3,6 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.toSet; -import java.io.IOException; import java.util.List; import java.util.Set; @@ -13,12 +12,10 @@ import org.gradle.api.tasks.options.Option; import io.quarkus.cli.commands.AddExtensions; +import io.quarkus.cli.commands.QuarkusCommandInvocation; import io.quarkus.cli.commands.file.GradleBuildFile; import io.quarkus.cli.commands.writer.FileProjectWriter; -/** - * @author Ståle Pedersen - */ public class QuarkusAddExtension extends QuarkusPlatformTask { public QuarkusAddExtension() { @@ -43,16 +40,17 @@ public void addExtension() { } @Override - protected void doExecute() { + protected void doExecute(QuarkusCommandInvocation invocation) { Set extensionsSet = getExtensionsToAdd() .stream() .flatMap(ext -> stream(ext.split(","))) .map(String::trim) .collect(toSet()); + invocation.setValue(AddExtensions.EXTENSIONS, extensionsSet); try { new AddExtensions(new GradleBuildFile(new FileProjectWriter(getProject().getProjectDir()))) - .addExtensions(extensionsSet); - } catch (IOException e) { + .execute(invocation); + } catch (Exception e) { throw new GradleException("Failed to add extensions " + getExtensionsToAdd(), e); } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java index 7097db1843a49..1a7115d7c3507 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java @@ -18,9 +18,6 @@ import io.quarkus.creator.CuratedApplicationCreator; import io.quarkus.creator.phase.augment.AugmentTask; -/** - * @author Ståle Pedersen - */ public class QuarkusBuild extends QuarkusTask { private boolean uberJar; @@ -31,7 +28,6 @@ public QuarkusBuild() { super("Quarkus builds a runner jar based on the build jar"); } - @Optional @Input public boolean isUberJar() { return uberJar; diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index 4c88072bf067d..ab99d14206fda 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -1,5 +1,7 @@ package io.quarkus.gradle.tasks; +import static java.util.stream.Collectors.joining; + import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; @@ -50,19 +52,16 @@ import org.gradle.api.tasks.options.Option; import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.bootstrap.model.AppDependency; import io.quarkus.bootstrap.model.AppModel; import io.quarkus.bootstrap.resolver.AppModelResolver; import io.quarkus.bootstrap.resolver.AppModelResolverException; -import io.quarkus.bootstrap.util.PropertyUtils; import io.quarkus.dev.DevModeContext; import io.quarkus.dev.DevModeMain; import io.quarkus.gradle.QuarkusPluginExtension; import io.quarkus.utilities.JavaBinFinder; -/** - * @author Ståle Pedersen - */ public class QuarkusDev extends QuarkusTask { private Set filesIncludedInClasspath = new HashSet<>(); @@ -84,8 +83,9 @@ public QuarkusDev() { @InputDirectory @Optional public File getBuildDir() { - if (buildDir == null) + if (buildDir == null) { buildDir = getProject().getBuildDir(); + } return buildDir; } @@ -96,10 +96,11 @@ public void setBuildDir(File buildDir) { @Optional @InputDirectory public File getSourceDir() { - if (sourceDir == null) + if (sourceDir == null) { return extension().sourceDir(); - else + } else { return new File(sourceDir); + } } @Option(description = "Set source directory", option = "source-dir") @@ -110,10 +111,11 @@ public void setSourceDir(String sourceDir) { @Optional @InputDirectory public File getWorkingDir() { - if (workingDir == null) + if (workingDir == null) { return extension().workingDir(); - else + } else { return new File(workingDir); + } } @Option(description = "Set working directory", option = "working-dir") @@ -132,7 +134,6 @@ public void setJvmArgs(String jvmArgs) { this.jvmArgs = jvmArgs; } - @Optional @Input public boolean isPreventnoverify() { return preventnoverify; @@ -241,22 +242,13 @@ public void startDev() { throw new GradleException("Failed to resolve application model " + extension.getAppArtifact() + " dependencies", e); } - for (AppDependency appDep : appModel.getAllDependencies()) { - addToClassPaths(classPathManifest, context, appDep.getArtifact().getPath().toFile()); - } args.add("-Djava.util.logging.manager=org.jboss.logmanager.LogManager"); File wiringClassesDirectory = new File(getBuildDir(), "wiring-classes"); wiringClassesDirectory.mkdirs(); addToClassPaths(classPathManifest, context, wiringClassesDirectory); - //we also want to add the maven plugin jar to the class path - //this allows us to just directly use classes, without messing around copying them - //to the runner jar - addGradlePluginDeps(classPathManifest, context); - //now we need to build a temporary jar to actually run - File tempFile = new File(getBuildDir(), extension.finalName() + "-dev.jar"); tempFile.delete(); tempFile.deleteOnExit(); @@ -264,13 +256,16 @@ public void startDev() { StringBuilder resources = new StringBuilder(); String res = null; for (File file : extension.resourcesDir()) { - if (resources.length() > 0) + if (resources.length() > 0) { resources.append(File.pathSeparator); + } resources.append(file.getAbsolutePath()); res = file.getAbsolutePath(); } - final Configuration compileCp = project.getConfigurations().getByName("compileClasspath"); + final Set projectDependencies = new HashSet<>(); + final Configuration compileCp = project.getConfigurations() + .getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME); final DependencySet compileCpDependencies = compileCp.getAllDependencies(); for (Dependency dependency : compileCpDependencies) { @@ -278,6 +273,11 @@ public void startDev() { continue; } + // Create the key via AppArtifact to make sure we use same defaults for type and classifier + AppArtifactKey key = new AppArtifact(dependency.getGroup(), dependency.getName(), dependency.getVersion()) + .getKey(); + projectDependencies.add(key); + Project dependencyProject = ((ProjectDependency) dependency).getDependencyProject(); Convention convention = dependencyProject.getConvention(); JavaPluginConvention javaConvention = convention.findPlugin(JavaPluginConvention.class); @@ -306,6 +306,17 @@ public void startDev() { context.getModules().add(wsModuleInfo); } + for (AppDependency appDependency : appModel.getAllDependencies()) { + if (!projectDependencies.contains(appDependency.getArtifact().getKey())) { + addToClassPaths(classPathManifest, context, appDependency.getArtifact().getPath().toFile()); + } + } + + //we also want to add the maven plugin jar to the class path + //this allows us to just directly use classes, without messing around copying them + //to the runner jar + addGradlePluginDeps(classPathManifest, context); + DevModeContext.ModuleInfo moduleInfo = new DevModeContext.ModuleInfo( project.getName(), project.getProjectDir().getAbsolutePath(), @@ -323,6 +334,8 @@ public void startDev() { context.setFrameworkClassesDir(wiringClassesDirectory.getAbsoluteFile()); context.setCacheDir(new File(getBuildDir(), "transformer-cache").getAbsoluteFile()); + // this is the jar file we will use to launch the dev mode main class + context.setDevModeRunnerJarFile(tempFile); try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(tempFile))) { out.putNextEntry(new ZipEntry("META-INF/")); Manifest manifest = new Manifest(); @@ -344,15 +357,13 @@ public void startDev() { args.add("-jar"); args.add(tempFile.getAbsolutePath()); - ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[0])); - pb.redirectErrorStream(true); - pb.redirectInput(ProcessBuilder.Redirect.INHERIT); - pb.directory(getWorkingDir()); - System.out.println("Starting process: "); - pb.command().forEach(System.out::println); - System.out.println("Args: "); - args.forEach(System.out::println); - + ProcessBuilder pb = new ProcessBuilder(args) + .redirectErrorStream(true) + .redirectInput(ProcessBuilder.Redirect.INHERIT) + .directory(getWorkingDir()); + if (getLogger().isDebugEnabled()) { + getLogger().debug("Launching JVM with command line: {}", pb.command().stream().collect(joining(" "))); + } Process p = pb.start(); Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override @@ -363,7 +374,7 @@ public void run() { try { ExecutorService es = Executors.newSingleThreadExecutor(); es.submit(() -> copyOutputToConsole(p.getInputStream())); - + es.shutdown(); p.waitFor(); } catch (Exception e) { p.destroy(); @@ -398,7 +409,7 @@ private void copyOutputToConsole(InputStream is) { private void addGradlePluginDeps(StringBuilder classPathManifest, DevModeContext context) { Configuration conf = getProject().getBuildscript().getConfigurations().getByName("classpath"); ResolvedDependency quarkusDep = conf.getResolvedConfiguration().getFirstLevelModuleDependencies().stream() - .filter(rd -> "quarkus-gradle-plugin".equals(rd.getModuleName())) + .filter(rd -> "io.quarkus.gradle.plugin".equals(rd.getModuleName())) .findFirst() .orElseThrow(() -> new IllegalStateException("Unable to find quarkus-gradle-plugin dependency")); @@ -411,19 +422,9 @@ private void addToClassPaths(StringBuilder classPathManifest, DevModeContext con if (filesIncludedInClasspath.add(file)) { getProject().getLogger().info("Adding dependency {}", file); - URI uri = file.toPath().toAbsolutePath().toUri(); - String path = uri.getRawPath(); - if (PropertyUtils.isWindows()) { - if (path.length() > 2 && Character.isLetter(path.charAt(0)) && path.charAt(1) == ':') { - path = "/" + path; - } - } - classPathManifest.append(path); + final URI uri = file.toPath().toAbsolutePath().toUri(); context.getClassPath().add(toUrl(uri)); - if (file.isDirectory()) { - classPathManifest.append("/"); - } - classPathManifest.append(" "); + classPathManifest.append(uri).append(" "); } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java index 3850f4797dda4..7f0f2223f671b 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java @@ -1,7 +1,5 @@ package io.quarkus.gradle.tasks; -import java.io.IOException; - import org.gradle.api.GradleException; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; @@ -9,12 +7,10 @@ import org.gradle.api.tasks.options.Option; import io.quarkus.cli.commands.ListExtensions; +import io.quarkus.cli.commands.QuarkusCommandInvocation; import io.quarkus.cli.commands.writer.FileProjectWriter; import io.quarkus.gradle.GradleBuildFileFromConnector; -/** - * @author Ståle Pedersen - */ public class QuarkusListExtensions extends QuarkusPlatformTask { private boolean all = true; @@ -23,7 +19,6 @@ public class QuarkusListExtensions extends QuarkusPlatformTask { private String searchPattern; - @Optional @Input public boolean isAll() { return all; @@ -66,13 +61,14 @@ public void listExtensions() { } @Override - protected void doExecute() { + protected void doExecute(QuarkusCommandInvocation invocation) { + invocation.setValue(ListExtensions.ALL, isAll()) + .setValue(ListExtensions.FORMAT, getFormat()) + .setValue(ListExtensions.SEARCH, getSearchPattern()); try { new ListExtensions(new GradleBuildFileFromConnector(new FileProjectWriter(getProject().getProjectDir()))) - .listExtensions( - isAll(), - getFormat(), getSearchPattern()); - } catch (IOException e) { + .execute(invocation); + } catch (Exception e) { throw new GradleException("Unable to list extensions", e); } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusNative.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusNative.java index b1b95b4727f17..0c34eb843cfe0 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusNative.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusNative.java @@ -24,9 +24,6 @@ import io.quarkus.creator.CuratedApplicationCreator; import io.quarkus.creator.phase.augment.AugmentTask; -/** - * @author Ståle Pedersen - */ public class QuarkusNative extends QuarkusTask { private boolean reportErrorsAtRuntime = false; @@ -81,7 +78,6 @@ public QuarkusNative() { super("Building a native image"); } - @Optional @Input public boolean isAddAllCharsets() { return addAllCharsets; @@ -92,7 +88,6 @@ public void setAddAllCharsets(final boolean addAllCharsets) { this.addAllCharsets = addAllCharsets; } - @Optional @Input public boolean isReportErrorsAtRuntime() { return reportErrorsAtRuntime; @@ -103,7 +98,6 @@ public void setReportErrorsAtRuntime(boolean reportErrorsAtRuntime) { this.reportErrorsAtRuntime = reportErrorsAtRuntime; } - @Optional @Input public boolean isDebugSymbols() { return debugSymbols; @@ -114,7 +108,6 @@ public void setDebugSymbols(boolean debugSymbols) { this.debugSymbols = debugSymbols; } - @Optional @Input public boolean isDebugBuildProcess() { return debugBuildProcess; @@ -125,7 +118,6 @@ public void setDebugBuildProcess(boolean debugBuildProcess) { this.debugBuildProcess = debugBuildProcess; } - @Optional @Input public boolean isCleanupServer() { return cleanupServer; @@ -136,15 +128,13 @@ public void setCleanupServer(boolean cleanupServer) { this.cleanupServer = cleanupServer; } - @Optional @Input public boolean isEnableHttpUrlHandler() { return enableHttpUrlHandler; } - @Optional @Input - private boolean isEnableFallbackImages() { + public boolean isEnableFallbackImages() { return enableFallbackImages; } @@ -160,7 +150,6 @@ public void setEnableHttpUrlHandler(boolean enableHttpUrlHandler) { this.enableHttpUrlHandler = enableHttpUrlHandler; } - @Optional @Input public boolean isEnableHttpsUrlHandler() { return enableHttpsUrlHandler; @@ -171,7 +160,6 @@ public void setEnableHttpsUrlHandler(boolean enableHttpsUrlHandler) { this.enableHttpsUrlHandler = enableHttpsUrlHandler; } - @Optional @Input public boolean isEnableAllSecurityServices() { return enableAllSecurityServices; @@ -182,7 +170,6 @@ public void setEnableAllSecurityServices(boolean enableAllSecurityServices) { this.enableAllSecurityServices = enableAllSecurityServices; } - @Optional @Input public boolean isEnableIsolates() { return enableIsolates; @@ -204,7 +191,6 @@ public void setGraalvmHome(String graalvmHome) { this.graalvmHome = graalvmHome; } - @Optional @Input public boolean isEnableServer() { return enableServer; @@ -215,7 +201,6 @@ public void setEnableServer(boolean enableServer) { this.enableServer = enableServer; } - @Optional @Input public boolean isEnableJni() { return enableJni; @@ -226,7 +211,6 @@ public void setEnableJni(boolean enableJni) { this.enableJni = enableJni; } - @Optional @Input public boolean isAutoServiceLoaderRegistration() { return autoServiceLoaderRegistration; @@ -237,7 +221,6 @@ public void setAutoServiceLoaderRegistration(boolean autoServiceLoaderRegistrati this.autoServiceLoaderRegistration = autoServiceLoaderRegistration; } - @Optional @Input public boolean isDumpProxies() { return dumpProxies; @@ -278,13 +261,11 @@ public String getDockerBuild() { } @Option(description = "Container runtime", option = "container-runtime") - @Optional public void setContainerRuntime(String containerRuntime) { this.containerRuntime = containerRuntime; } @Option(description = "Container runtime options", option = "container-runtime-options") - @Optional public void setContainerRuntimeOptions(String containerRuntimeOptions) { this.containerRuntimeOptions = containerRuntimeOptions; } @@ -294,7 +275,6 @@ public void setDockerBuild(String dockerBuild) { this.dockerBuild = dockerBuild; } - @Optional @Input public boolean isEnableVMInspection() { return enableVMInspection; @@ -305,7 +285,6 @@ public void setEnableVMInspection(boolean enableVMInspection) { this.enableVMInspection = enableVMInspection; } - @Optional @Input public boolean isFullStackTraces() { return fullStackTraces; @@ -316,7 +295,6 @@ public void setFullStackTraces(boolean fullStackTraces) { this.fullStackTraces = fullStackTraces; } - @Optional @Input public boolean isEnableReports() { return enableReports; @@ -344,7 +322,6 @@ public void setAdditionalBuildArgs(List additionalBuildArgs) { this.additionalBuildArgs = additionalBuildArgs; } - @Optional @Input public boolean isReportExceptionStackTraces() { return reportExceptionStackTraces; diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java index 324149d43d70c..fce4ef5b23af5 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java @@ -6,9 +6,10 @@ import java.nio.file.Path; import java.util.Properties; +import io.quarkus.cli.commands.QuarkusCommandInvocation; import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; import io.quarkus.platform.descriptor.resolver.json.QuarkusJsonPlatformDescriptorResolver; -import io.quarkus.platform.tools.config.QuarkusPlatformConfig; +import io.quarkus.platform.tools.MessageWriter; public abstract class QuarkusPlatformTask extends QuarkusTask { @@ -17,50 +18,36 @@ public abstract class QuarkusPlatformTask extends QuarkusTask { } protected void execute() { - try { - setupPlatformDescriptor(); - doExecute(); - } finally { - QuarkusPlatformConfig.clearThreadLocal(); - } + final MessageWriter msgWriter = new GradleMessageWriter(getProject().getLogger()); + final QuarkusPlatformDescriptor platformDescr = getPlatformDescriptor(msgWriter); + doExecute(new QuarkusCommandInvocation(platformDescr, msgWriter)); } - protected abstract void doExecute(); - - protected void setupPlatformDescriptor() { - - if (QuarkusPlatformConfig.hasThreadLocal()) { - getProject().getLogger().debug("Quarkus platform descriptor has already been initialized"); - return; - } else { - getProject().getLogger().debug("Initializing Quarkus platform descriptor"); - } + protected abstract void doExecute(QuarkusCommandInvocation invocation); + private QuarkusPlatformDescriptor getPlatformDescriptor(MessageWriter msgWriter) { final Path currentDir = getProject().getProjectDir().toPath(); final Path gradlePropsPath = currentDir.resolve("gradle.properties"); - if (Files.exists(gradlePropsPath)) { - final Properties props = new Properties(); - try (InputStream is = Files.newInputStream(gradlePropsPath)) { - props.load(is); - } catch (IOException e) { - throw new IllegalStateException("Failed to load " + gradlePropsPath, e); - } - - final QuarkusPlatformDescriptor platform = QuarkusJsonPlatformDescriptorResolver.newInstance() - .setArtifactResolver(extension().resolveAppModel()) - .setMessageWriter(new GradleMessageWriter(getProject().getLogger())) - .resolveFromBom( - getRequiredProperty(props, "quarkusPlatformGroupId"), - getRequiredProperty(props, "quarkusPlatformArtifactId"), - getRequiredProperty(props, "quarkusPlatformVersion")); - - QuarkusPlatformConfig.threadLocalConfigBuilder().setPlatformDescriptor(platform).build(); - - } else { + if (!Files.exists(gradlePropsPath)) { getProject().getLogger() .warn("Failed to locate " + gradlePropsPath + " to determine the Quarkus Platform BOM coordinates"); + return null; } + final Properties props = new Properties(); + try (InputStream is = Files.newInputStream(gradlePropsPath)) { + props.load(is); + } catch (IOException e) { + throw new IllegalStateException("Failed to load " + gradlePropsPath, e); + } + + return QuarkusJsonPlatformDescriptorResolver.newInstance() + .setArtifactResolver(extension().resolveAppModel()) + .setMessageWriter(msgWriter) + .resolveFromBom( + getRequiredProperty(props, "quarkusPlatformGroupId"), + getRequiredProperty(props, "quarkusPlatformArtifactId"), + getRequiredProperty(props, "quarkusPlatformVersion")); } private static String getRequiredProperty(Properties props, String name) { diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java index 157ba08fad5fc..1242768ed016c 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java @@ -4,9 +4,6 @@ import io.quarkus.gradle.QuarkusPluginExtension; -/** - * @author Ståle Pedersen - */ public abstract class QuarkusTask extends DefaultTask { private QuarkusPluginExtension extension; @@ -19,8 +16,9 @@ public abstract class QuarkusTask extends DefaultTask { } QuarkusPluginExtension extension() { - if (extension == null) - extension = (QuarkusPluginExtension) getProject().getExtensions().findByName("quarkus"); + if (extension == null) { + extension = getProject().getExtensions().findByType(QuarkusPluginExtension.class); + } return extension; } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTestNative.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTestNative.java index c043cdfad0c31..45217477ea613 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTestNative.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusTestNative.java @@ -5,6 +5,8 @@ import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.testing.Test; +import io.quarkus.gradle.QuarkusPlugin; + public class QuarkusTestNative extends Test { public QuarkusTestNative() { @@ -13,7 +15,7 @@ public QuarkusTestNative() { JavaPluginConvention javaPlugin = getProject().getConvention().getPlugin(JavaPluginConvention.class); SourceSetContainer sourceSets = javaPlugin.getSourceSets(); - SourceSet sourceSet = sourceSets.findByName("native-test"); + SourceSet sourceSet = sourceSets.findByName(QuarkusPlugin.NATIVE_TEST_SOURCE_SET_NAME); setTestClassesDirs(sourceSet.getOutput().getClassesDirs()); setClasspath(sourceSet.getRuntimeClasspath()); diff --git a/devtools/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java b/devtools/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java index 3270b0c674089..337df0ded4573 100644 --- a/devtools/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java +++ b/devtools/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java @@ -1,9 +1,12 @@ package io.quarkus.gradle; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.gradle.api.Project; +import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.tasks.TaskContainer; import org.gradle.testfixtures.ProjectBuilder; import org.junit.jupiter.api.Test; @@ -13,38 +16,41 @@ public class QuarkusPluginTest { @Test public void shouldCreateTasks() { Project project = ProjectBuilder.builder().build(); - project.getPluginManager().apply("io.quarkus"); + project.getPluginManager().apply(QuarkusPlugin.ID); - assertTrue(project.getPluginManager().hasPlugin("io.quarkus")); + assertTrue(project.getPluginManager().hasPlugin(QuarkusPlugin.ID)); TaskContainer tasks = project.getTasks(); - assertNotNull(tasks.getByName("quarkusBuild")); - assertNotNull(tasks.getByName("quarkusDev")); - assertNotNull(tasks.getByName("buildNative")); - assertNotNull(tasks.getByName("listExtensions")); - assertNotNull(tasks.getByName("addExtension")); + assertNotNull(tasks.getByName(QuarkusPlugin.QUARKUS_BUILD_TASK_NAME)); + assertNotNull(tasks.getByName(QuarkusPlugin.QUARKUS_DEV_TASK_NAME)); + assertNotNull(tasks.getByName(QuarkusPlugin.BUILD_NATIVE_TASK_NAME)); + assertNotNull(tasks.getByName(QuarkusPlugin.LIST_EXTENSIONS_TASK_NAME)); + assertNotNull(tasks.getByName(QuarkusPlugin.ADD_EXTENSION_TASK_NAME)); } @Test public void shouldMakeAssembleDependOnQuarkusBuild() { Project project = ProjectBuilder.builder().build(); - project.getPluginManager().apply("io.quarkus"); + project.getPluginManager().apply(QuarkusPlugin.ID); project.getPluginManager().apply("base"); TaskContainer tasks = project.getTasks(); - assertTrue(tasks.getByName("assemble").getDependsOn().contains(tasks.getByName("quarkusBuild"))); + assertThat(tasks.getByName(BasePlugin.ASSEMBLE_TASK_NAME).getDependsOn()) + .contains(tasks.getByName(QuarkusPlugin.QUARKUS_BUILD_TASK_NAME)); } @Test public void shouldMakeQuarkusDevAndQuarkusBuildDependOnClassesTask() { Project project = ProjectBuilder.builder().build(); - project.getPluginManager().apply("io.quarkus"); + project.getPluginManager().apply(QuarkusPlugin.ID); project.getPluginManager().apply("java"); TaskContainer tasks = project.getTasks(); - assertTrue(tasks.getByName("quarkusBuild").getDependsOn().contains(tasks.getByName("classes"))); - assertTrue(tasks.getByName("quarkusDev").getDependsOn().contains(tasks.getByName("classes"))); + assertThat(tasks.getByName(QuarkusPlugin.QUARKUS_BUILD_TASK_NAME).getDependsOn()) + .contains(tasks.getByName(JavaPlugin.CLASSES_TASK_NAME)); + assertThat(tasks.getByName(QuarkusPlugin.QUARKUS_DEV_TASK_NAME).getDependsOn()) + .contains(tasks.getByName(JavaPlugin.CLASSES_TASK_NAME)); } } diff --git a/devtools/gradle/src/test/resources/gradle-project/build.gradle b/devtools/gradle/src/test/resources/gradle-project/build.gradle index bfd1554070a34..2e6db65bd7396 100644 --- a/devtools/gradle/src/test/resources/gradle-project/build.gradle +++ b/devtools/gradle/src/test/resources/gradle-project/build.gradle @@ -5,9 +5,7 @@ plugins { } repositories { - maven { - url uri(System.getenv('MAVEN_LOCAL_REPO')) - } + mavenLocal() mavenCentral() } diff --git a/devtools/maven/pom.xml b/devtools/maven/pom.xml index 557c564b843e2..876e6320b2cdd 100644 --- a/devtools/maven/pom.xml +++ b/devtools/maven/pom.xml @@ -21,12 +21,6 @@ io.quarkus quarkus-bootstrap-core - - - org.apache.maven.wagon - wagon-provider-api - - io.quarkus @@ -58,10 +52,6 @@ jakarta.enterprise jakarta.enterprise.cdi-api - - org.apache.maven.shared - maven-invoker - org.apache.maven maven-core @@ -72,21 +62,6 @@ - - org.apache.maven - maven-toolchain - - - commons-logging - commons-logging-api - - - log4j - log4j - - - - org.apache.maven.plugin-tools maven-plugin-annotations diff --git a/devtools/maven/src/main/java/io/quarkus/maven/AnalyseCallTreeMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/AnalyseCallTreeMojo.java index 0c921b16197df..caf115e197459 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/AnalyseCallTreeMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/AnalyseCallTreeMojo.java @@ -35,9 +35,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { String clazz = className; String method = ""; if (methodName != null) { - int idex = methodName.lastIndexOf('.'); - clazz = methodName.substring(0, idex); - method = methodName.substring(idex + 1); + int index = methodName.lastIndexOf('.'); + clazz = methodName.substring(0, index); + method = methodName.substring(index + 1); } File[] files = reportsDir.listFiles(); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/BuildFileMojoBase.java b/devtools/maven/src/main/java/io/quarkus/maven/BuildFileMojoBase.java index c985e767a5b8e..1acebb786f885 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/BuildFileMojoBase.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/BuildFileMojoBase.java @@ -86,23 +86,20 @@ public void execute() throws MojoExecutionException { continue; } // We don't know which BOM is the platform one, so we are trying every BOM here - String bomVersion = dep.getVersion(); - if (bomVersion.startsWith("${") && bomVersion.endsWith("}")) { - final String prop = bomVersion.substring(2, bomVersion.length() - 1); - bomVersion = mvnBuild.getProperty(prop); - if (bomVersion == null) { - getLog().debug("Failed to resolve version of " + dep); - continue; - } + final String bomVersion = resolveValue(dep.getVersion(), buildFile); + final String bomGroupId = resolveValue(dep.getGroupId(), buildFile); + final String bomArtifactId = resolveValue(dep.getArtifactId(), buildFile); + if (bomVersion == null || bomGroupId == null || bomArtifactId == null) { + continue; } - Artifact jsonArtifact = new DefaultArtifact(dep.getGroupId(), dep.getArtifactId(), dep.getClassifier(), - "json", bomVersion); + + Artifact jsonArtifact = new DefaultArtifact(bomGroupId, bomArtifactId, null, "json", bomVersion); try { jsonArtifact = mvn.resolve(jsonArtifact).getArtifact(); } catch (Exception e) { log.debug("Failed to resolve JSON descriptor as %s", jsonArtifact); - jsonArtifact = new DefaultArtifact(dep.getGroupId(), dep.getArtifactId() + "-descriptor-json", - dep.getClassifier(), "json", bomVersion); + jsonArtifact = new DefaultArtifact(bomGroupId, bomArtifactId + "-descriptor-json", null, "json", + bomVersion); try { jsonArtifact = mvn.resolve(jsonArtifact).getArtifact(); } catch (Exception e1) { @@ -113,7 +110,6 @@ public void execute() throws MojoExecutionException { descrArtifact = jsonArtifact; break; } - if (descrArtifact != null) { log.debug("Quarkus platform JSON descriptor resolved from %s", descrArtifact); final QuarkusPlatformDescriptor platform = QuarkusJsonPlatformDescriptorResolver.newInstance() @@ -153,4 +149,15 @@ protected void validateParameters() throws MojoExecutionException { } protected abstract void doExecute(BuildFile buildFile) throws MojoExecutionException; + + private String resolveValue(String expr, BuildFile buildFile) throws IOException { + if (expr.startsWith("${") && expr.endsWith("}")) { + final String v = buildFile.getProperty(expr.substring(2, expr.length() - 1)); + if (v == null) { + getLog().debug("Failed to resolve version of " + v); + } + return v; + } + return expr; + } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java index d218a1196d8da..b73e0445db542 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java @@ -94,12 +94,6 @@ public class BuildMojo extends AbstractMojo { @Parameter(defaultValue = "${project.build.directory}") private File buildDir; - /** - * The directory for library jars - */ - @Parameter(defaultValue = "${project.build.directory}/lib") - private File libDir; - @Parameter(defaultValue = "${project.build.finalName}") private String finalName; diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java index 36ccd6ddd3643..525dbeb34b7d4 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; @@ -54,7 +55,8 @@ import io.quarkus.generators.SourceType; import io.quarkus.maven.components.MavenVersionEnforcer; import io.quarkus.maven.components.Prompter; -import io.quarkus.maven.utilities.MojoUtils; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.platform.tools.ToolsUtils; /** * This goal helps in setting up Quarkus Maven project with quarkus-maven-plugin, with sensible defaults @@ -147,7 +149,8 @@ public void execute() throws MojoExecutionException { } catch (AppModelResolverException e1) { throw new MojoExecutionException("Failed to initialize Maven artifact resolver", e1); } - CreateUtils.setGlobalPlatformDescriptor(bomGroupId, bomArtifactId, bomVersion, mvn, getLog()); + final QuarkusPlatformDescriptor platform = CreateUtils.setGlobalPlatformDescriptor(bomGroupId, bomArtifactId, + bomVersion, mvn, getLog()); // We detect the Maven version during the project generation to indicate the user immediately that the installed // version may not be supported. @@ -179,7 +182,7 @@ public void execute() throws MojoExecutionException { projectRoot = new File(outputDirectory, projectArtifactId); if (projectRoot.exists()) { throw new MojoExecutionException("Unable to create the project, " + - " the directory " + projectRoot.getAbsolutePath() + " already exists"); + "the directory " + projectRoot.getAbsolutePath() + " already exists"); } } @@ -221,12 +224,12 @@ public void execute() throws MojoExecutionException { } } if (BuildTool.MAVEN.equals(buildToolEnum)) { - createMavenWrapper(createdDependenciesBuildFile); + createMavenWrapper(createdDependenciesBuildFile, ToolsUtils.readQuarkusProperties(platform)); } else if (BuildTool.GRADLE.equals(buildToolEnum)) { - createGradleWrapper(buildFile.getParentFile()); + createGradleWrapper(buildFile.getParentFile(), ToolsUtils.readQuarkusProperties(platform)); } } catch (IOException e) { - throw new MojoExecutionException(e.getMessage(), e); + throw new MojoExecutionException("Failed to generate Quarkus project", e); } if (success) { printUserInstructions(projectRoot); @@ -236,11 +239,11 @@ public void execute() throws MojoExecutionException { } } - private void createGradleWrapper(File projectDirectory) { + private void createGradleWrapper(File projectDirectory, Properties props) { try { String gradleName = IS_WINDOWS ? "gradle.bat" : "gradle"; ProcessBuilder pb = new ProcessBuilder(gradleName, "wrapper", - "--gradle-version=" + MojoUtils.getGradleWrapperVersion()).directory(projectDirectory) + "--gradle-version=" + ToolsUtils.getGradleWrapperVersion(props)).directory(projectDirectory) .inheritIO(); Process x = pb.start(); @@ -259,7 +262,7 @@ private void createGradleWrapper(File projectDirectory) { } - private void createMavenWrapper(File createdPomFile) { + private void createMavenWrapper(File createdPomFile, Properties props) { try { // we need to modify the maven environment used by the wrapper plugin since the project could have been // created in a directory other than the current @@ -277,10 +280,10 @@ private void createMavenWrapper(File createdPomFile) { plugin( groupId("io.takari"), artifactId("maven"), - version(MojoUtils.getMavenWrapperVersion())), + version(ToolsUtils.getMavenWrapperVersion(props))), goal("wrapper"), configuration( - element(name("maven"), MojoUtils.getProposedMavenVersion())), + element(name("maven"), ToolsUtils.getProposedMavenVersion(props))), executionEnvironment( newProject, newSession, diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateUtils.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateUtils.java index a8904865d6f25..2c4ffe167e162 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateUtils.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateUtils.java @@ -50,7 +50,8 @@ private static boolean isVersionRange(String versionStr) { return versionStr.indexOf(',') >= 0; } - static void setGlobalPlatformDescriptor(final String bomGroupId, final String bomArtifactId, final String bomVersion, + static QuarkusPlatformDescriptor setGlobalPlatformDescriptor(final String bomGroupId, final String bomArtifactId, + final String bomVersion, MavenArtifactResolver mvn, Log log) throws MojoExecutionException { final QuarkusJsonPlatformDescriptorResolver platformResolver = QuarkusJsonPlatformDescriptorResolver.newInstance() @@ -80,6 +81,7 @@ static void setGlobalPlatformDescriptor(final String bomGroupId, final String bo } QuarkusPlatformConfig.defaultConfigBuilder().setPlatformDescriptor(platform).build(); + return platform; } public static String getDerivedPath(String className) { diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index afafff887fe9d..7b41179947a64 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -1,5 +1,7 @@ package io.quarkus.maven; +import static java.util.stream.Collectors.joining; + import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; @@ -30,8 +32,10 @@ import java.util.zip.ZipOutputStream; import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.BuildPluginManager; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; @@ -40,22 +44,23 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; -import org.apache.maven.shared.invoker.DefaultInvocationRequest; -import org.apache.maven.shared.invoker.InvocationRequest; -import org.apache.maven.shared.invoker.Invoker; -import org.apache.maven.shared.invoker.MavenInvocationException; -import org.apache.maven.toolchain.ToolchainManager; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.WorkspaceReader; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.twdata.maven.mojoexecutor.MojoExecutor; import io.quarkus.bootstrap.model.AppDependency; import io.quarkus.bootstrap.model.AppModel; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; +import io.quarkus.bootstrap.resolver.maven.MavenRepoInitializer; import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; -import io.quarkus.bootstrap.util.PropertyUtils; import io.quarkus.dev.DevModeContext; import io.quarkus.dev.DevModeMain; import io.quarkus.maven.components.MavenVersionEnforcer; @@ -70,6 +75,33 @@ */ @Mojo(name = "dev", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) public class DevMojo extends AbstractMojo { + + /** + * running any one of these phases means the compile phase will have been run, if these have + * not been run we manually run compile + */ + private static final Set POST_COMPILE_PHASES = new HashSet<>(Arrays.asList( + "compile", + "process-classes", + "generate-test-sources", + "process-test-sources", + "generate-test-resources", + "process-test-resources", + "test-compile", + "process-test-classes", + "test", + "prepare-package", + "package", + "pre-integration-test", + "integration-test", + "post-integration-test", + "verify", + "install", + "deploy")); + + private static final String ORG_APACHE_MAVEN_PLUGINS = "org.apache.maven.plugins"; + private static final String MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin"; + /** * The directory for compiled classes. */ @@ -155,9 +187,6 @@ public class DevMojo extends AbstractMojo { @Component private RepositorySystem repoSystem; - @Component - private Invoker invoker; - @Parameter(defaultValue = "${repositorySystemSession}", readonly = true) private RepositorySystemSession repoSession; @@ -198,18 +227,14 @@ public class DevMojo extends AbstractMojo { private String target; @Component - private ToolchainManager toolchainManager; - - public ToolchainManager getToolchainManager() { - return toolchainManager; - } + private WorkspaceReader wsReader; - public MavenSession getSession() { - return session; - } + @Component + private BuildPluginManager pluginManager; @Override public void execute() throws MojoFailureException, MojoExecutionException { + mavenVersionEnforcer.ensureMavenVersion(getLog(), session); boolean found = MojoUtils.checkProjectForMavenBuildPlugin(project); @@ -223,17 +248,37 @@ public void execute() throws MojoFailureException, MojoExecutionException { if (!sourceDir.isDirectory()) { getLog().warn("The project's sources directory does not exist " + sourceDir); } + //we check to see if there was a compile (or later) goal before this plugin + boolean compileNeeded = true; + for (String goal : session.getGoals()) { + if (POST_COMPILE_PHASES.contains(goal)) { + compileNeeded = false; + break; + } + if (goal.endsWith("quarkus:dev")) { + break; + } + } - if (!buildDir.isDirectory() || !new File(buildDir, "classes").isDirectory()) { - try { - InvocationRequest request = new DefaultInvocationRequest(); - request.setBatchMode(true); - request.setGoals(Collections.singletonList("compile")); - - invoker.execute(request); - } catch (MavenInvocationException e) { - throw new MojoExecutionException(e.getMessage(), e); + //if the user did not compile we run it for them + if (compileNeeded) { + // Compile the project + final String key = ORG_APACHE_MAVEN_PLUGINS + ":" + MAVEN_COMPILER_PLUGIN; + final Plugin plugin = project.getPlugin(key); + if (plugin == null) { + throw new MojoExecutionException("Failed to locate " + key + " among the project plugins"); } + MojoExecutor.executeMojo( + MojoExecutor.plugin( + MojoExecutor.groupId(ORG_APACHE_MAVEN_PLUGINS), + MojoExecutor.artifactId(MAVEN_COMPILER_PLUGIN), + MojoExecutor.version(plugin.getVersion())), + MojoExecutor.goal("compile"), + MojoExecutor.configuration(), + MojoExecutor.executionEnvironment( + project, + session, + pluginManager)); } try { @@ -381,27 +426,18 @@ private void addProject(DevModeContext devModeContext, LocalProject localProject } private void addToClassPaths(StringBuilder classPathManifest, DevModeContext classPath, File file) { - URI uri = file.toPath().toAbsolutePath().toUri(); + final URI uri = file.toPath().toAbsolutePath().toUri(); try { classPath.getClassPath().add(uri.toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } - String path = uri.getRawPath(); - if (PropertyUtils.isWindows()) { - if (path.length() > 2 && Character.isLetter(path.charAt(0)) && path.charAt(1) == ':') { - path = "/" + path; - } - } - classPathManifest.append(path); - if (file.isDirectory() && path.charAt(path.length() - 1) != '/') { - classPathManifest.append("/"); - } - classPathManifest.append(" "); + classPathManifest.append(uri).append(" "); } class DevModeRunner { + private static final String KOTLIN_MAVEN_PLUGIN_GA = "org.jetbrains.kotlin:kotlin-maven-plugin"; private final List args; private Process process; private Set pomFiles = new HashSet<>(); @@ -454,7 +490,13 @@ void prepare() throws Exception { for (Map.Entry e : System.getProperties().entrySet()) { devModeContext.getSystemProperties().put(e.getKey().toString(), (String) e.getValue()); } + devModeContext.getBuildSystemProperties().putAll((Map) project.getProperties()); + + // this is a minor hack to allow ApplicationConfig to be populated with defaults + devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.application.name", project.getArtifactId()); + devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.application.version", project.getVersion()); + devModeContext.setSourceEncoding(getSourceEncoding()); devModeContext.setSourceJavaVersion(source); devModeContext.setTargetJvmVersion(target); @@ -485,12 +527,16 @@ void prepare() throws Exception { } } + setKotlinSpecificFlags(devModeContext); + final AppModel appModel; try { + RepositorySystem repoSystem = DevMojo.this.repoSystem; final LocalProject localProject; if (noDeps) { localProject = LocalProject.load(outputDirectory.toPath()); addProject(devModeContext, localProject); + pomFiles.add(localProject.getDir().resolve("pom.xml")); } else { localProject = LocalProject.loadWorkspace(outputDirectory.toPath()); for (LocalProject project : localProject.getSelfWithLocalDeps()) { @@ -504,20 +550,10 @@ void prepare() throws Exception { } } addProject(devModeContext, project); + pomFiles.add(project.getDir().resolve("pom.xml")); } + repoSystem = MavenRepoInitializer.getRepositorySystem(repoSession.isOffline(), localProject.getWorkspace()); } - for (LocalProject i : localProject.getSelfWithLocalDeps()) { - pomFiles.add(i.getDir().resolve("pom.xml")); - } - - /* - * TODO: support multiple resources dirs for config hot deployment - * String resources = null; - * for (Resource i : project.getBuild().getResources()) { - * resources = i.getDirectory(); - * break; - * } - */ appModel = new BootstrapAppModelResolver(MavenArtifactResolver.builder() .setRepositorySystem(repoSystem) @@ -611,20 +647,65 @@ void prepare() throws Exception { } + private void setKotlinSpecificFlags(DevModeContext devModeContext) { + Plugin kotlinMavenPlugin = null; + for (Plugin plugin : project.getBuildPlugins()) { + if (plugin.getKey().equals(KOTLIN_MAVEN_PLUGIN_GA)) { + kotlinMavenPlugin = plugin; + break; + } + } + + if (kotlinMavenPlugin == null) { + return; + } + + getLog().debug("Kotlin Maven plugin detected"); + + List compilerPluginArtifacts = new ArrayList<>(); + List dependencies = kotlinMavenPlugin.getDependencies(); + for (Dependency dependency : dependencies) { + try { + ArtifactResult resolvedArtifact = repoSystem.resolveArtifact(repoSession, + new ArtifactRequest() + .setArtifact(new DefaultArtifact(dependency.getGroupId(), dependency.getArtifactId(), + dependency.getClassifier(), dependency.getType(), dependency.getVersion())) + .setRepositories(repos)); + + compilerPluginArtifacts.add(resolvedArtifact.getArtifact().getFile().toPath().toAbsolutePath().toString()); + } catch (ArtifactResolutionException e) { + getLog().warn("Unable to properly setup dev-mode for Kotlin", e); + return; + } + } + devModeContext.setCompilerPluginArtifacts(compilerPluginArtifacts); + + List options = new ArrayList<>(); + Xpp3Dom compilerPluginConfiguration = (Xpp3Dom) kotlinMavenPlugin.getConfiguration(); + if (compilerPluginConfiguration != null) { + Xpp3Dom compilerPluginArgsConfiguration = compilerPluginConfiguration.getChild("pluginOptions"); + if (compilerPluginArgsConfiguration != null) { + for (Xpp3Dom argConfiguration : compilerPluginArgsConfiguration.getChildren()) { + options.add(argConfiguration.getValue()); + } + } + } + devModeContext.setCompilerPluginsOptions(options); + } + public Set getPomFiles() { return pomFiles; } public void run() throws Exception { // Display the launch command line in dev mode - getLog().info("Launching JVM with command line: " + args.toString()); - ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[0])); - pb.redirectError(ProcessBuilder.Redirect.INHERIT); - pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); - pb.redirectInput(ProcessBuilder.Redirect.INHERIT); - pb.directory(workingDir); - process = pb.start(); - + if (getLog().isDebugEnabled()) { + getLog().debug("Launching JVM with command line: " + args.stream().collect(joining(" "))); + } + process = new ProcessBuilder(args) + .inheritIO() + .directory(workingDir) + .start(); //https://github.com/quarkusio/quarkus/issues/232 Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override diff --git a/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java index c4c57bf92dd9c..688d6250a4390 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java @@ -20,12 +20,15 @@ import org.apache.maven.toolchain.ToolchainManager; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import io.quarkus.maven.components.MavenVersionEnforcer; import io.quarkus.maven.utilities.MojoUtils; import io.quarkus.remotedev.AgentRunner; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.configuration.QuarkusConfigFactory; import io.smallrye.config.PropertiesConfigSource; -import io.smallrye.config.SmallRyeConfigProviderResolver; +import io.smallrye.config.SmallRyeConfig; /** * The dev mojo, that connects to a remote host. @@ -101,13 +104,15 @@ public void execute() throws MojoFailureException, MojoExecutionException { Path config = Paths.get(resources).resolve("application.properties"); if (Files.exists(config)) { try { - Config built = SmallRyeConfigProviderResolver.instance().getBuilder() - .addDefaultSources() - .addDiscoveredConverters() - .addDiscoveredSources() + SmallRyeConfig built = ConfigUtils.configBuilder(false) .withSources(new PropertiesConfigSource(config.toUri().toURL())).build(); - SmallRyeConfigProviderResolver.instance().registerConfig(built, - Thread.currentThread().getContextClassLoader()); + QuarkusConfigFactory.setConfig(built); + final ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + final Config existing = cpr.getConfig(); + if (existing != built) { + cpr.releaseConfig(existing); + // subsequent calls will get the new config + } } catch (Exception e) { throw new RuntimeException(e); } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/components/MavenVersionEnforcer.java b/devtools/maven/src/main/java/io/quarkus/maven/components/MavenVersionEnforcer.java index 2230406a9b00b..406799e676320 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/components/MavenVersionEnforcer.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/components/MavenVersionEnforcer.java @@ -1,6 +1,9 @@ package io.quarkus.maven.components; +import java.io.IOException; +import java.io.InputStream; import java.util.List; +import java.util.Properties; import org.apache.commons.lang3.StringUtils; import org.apache.maven.artifact.versioning.*; @@ -9,19 +12,43 @@ import org.apache.maven.plugin.logging.Log; import org.codehaus.plexus.component.annotations.Component; -import io.quarkus.maven.utilities.MojoUtils; - @Component(role = MavenVersionEnforcer.class, instantiationStrategy = "per-lookup") public class MavenVersionEnforcer { public void ensureMavenVersion(Log log, MavenSession session) throws MojoExecutionException { - String supported = MojoUtils.get("supported-maven-versions"); + final String supported; + try { + supported = getSupportedMavenVersions(); + } catch (IOException e) { + throw new MojoExecutionException("Failed to ensure Quarkus Maven version compatibility", e); + } String mavenVersion = session.getSystemProperties().getProperty("maven.version"); - log.debug("Detected Maven Version: " + mavenVersion); + if (log.isDebugEnabled()) { + log.debug("Detected Maven Version: " + mavenVersion); + } DefaultArtifactVersion detectedVersion = new DefaultArtifactVersion(mavenVersion); enforce(log, supported, detectedVersion); } + private static String getSupportedMavenVersions() throws IOException { + return loadQuarkusProperties().getProperty("supported-maven-versions"); + } + + private static Properties loadQuarkusProperties() throws IOException { + final String resource = "quarkus.properties"; + final InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource); + if (is == null) { + throw new IOException("Could not locate " + resource + " on the classpath"); + } + final Properties props = new Properties(); + try { + props.load(is); + } catch (IOException e) { + throw new IOException("Failed to load " + resource + " from the classpath", e); + } + return props; + } + /** * Compares the specified Maven version to see if it is allowed by the defined version range. * @@ -35,27 +62,23 @@ private void enforce(Log log, throws MojoExecutionException { if (StringUtils.isBlank(requiredMavenVersionRange)) { throw new MojoExecutionException("Maven version can't be empty."); - } else { - VersionRange vr; - String msg = "Detected Maven Version (" + actualMavenVersion + ") "; - - if (actualMavenVersion.toString().equals(requiredMavenVersionRange)) { - log.debug(msg + " is allowed in " + requiredMavenVersionRange + "."); - } else { - try { - vr = VersionRange.createFromVersionSpec(requiredMavenVersionRange); - if (containsVersion(vr, actualMavenVersion)) { - log.debug(msg + " is allowed in " + requiredMavenVersionRange + "."); - } else { - String message = msg + " is not supported, it must be in " + vr + "."; - throw new MojoExecutionException(message); - } - } catch (InvalidVersionSpecificationException e) { - throw new MojoExecutionException("The requested Maven version " - + requiredMavenVersionRange + " is invalid.", e); + } + if (!actualMavenVersion.toString().equals(requiredMavenVersionRange)) { + try { + final VersionRange vr = VersionRange.createFromVersionSpec(requiredMavenVersionRange); + if (!containsVersion(vr, actualMavenVersion)) { + throw new MojoExecutionException(getDetectedVersionStr(actualMavenVersion.toString()) + + " is not supported, it must be in " + vr + "."); } + } catch (InvalidVersionSpecificationException e) { + throw new MojoExecutionException("The requested Maven version " + + requiredMavenVersionRange + " is invalid.", e); } } + if (log.isDebugEnabled()) { + log.debug( + getDetectedVersionStr(actualMavenVersion.toString()) + " is allowed in " + requiredMavenVersionRange + "."); + } } /** @@ -84,4 +107,8 @@ private static boolean containsVersion(VersionRange allowedRange, ArtifactVersio } return matched; } + + private static String getDetectedVersionStr(String version) { + return "Detected Maven Version (" + version + ") "; + } } diff --git a/devtools/platform-descriptor-json/pom.xml b/devtools/platform-descriptor-json/pom.xml index 66d65a028a558..56f22a32510a0 100644 --- a/devtools/platform-descriptor-json/pom.xml +++ b/devtools/platform-descriptor-json/pom.xml @@ -22,23 +22,50 @@ src/main/filtered true - - ${project.basedir}/../../bom/runtime - quarkus-bom - true - - pom.xml - - - - ${project.basedir}/../bom-descriptor-json/target - quarkus-bom-descriptor - false - - extensions.json - - + + + maven-dependency-plugin + + + copy-bom + + copy + + + + + io.quarkus + quarkus-bom + ${project.version} + pom + ${project.build.outputDirectory}/quarkus-bom + pom.xml + + + + + + copy-bom-descriptor-json + + copy + + + + + io.quarkus + quarkus-bom-descriptor-json + ${project.version} + json + ${project.build.outputDirectory}/quarkus-bom-descriptor + extensions.json + + + + + + + diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/README.gradle.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/README.gradle.ftl new file mode 100644 index 0000000000000..3a846deb25056 --- /dev/null +++ b/devtools/platform-descriptor-json/src/main/resources/templates/README.gradle.ftl @@ -0,0 +1,35 @@ +# ${project_artifactId} project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +``` +./gradlew quarkusDev +``` + +## Packaging and running the application + +The application is packageable using `./gradlew quarkusBuild`. +It produces the executable `${project_artifactId}-${project_version}-runner.jar` file in `build` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/lib` directory. + +The application is now runnable using `java -jar build/${project_artifactId}-${project_version}-runner.jar`. + +If you want to build an _über-jar_, just add the `--uber-jar` option to the command line: +``` +./gradlew quarkusBuild --uber-jar +``` + +## Creating a native executable + +You can create a native executable using: `./gradlew buildNative`. + +Or you can use Docker to build the native executable using: `./gradlew buildNative --docker-build=true`. + +You can then execute your binary: `./build/${project_artifactId}-${project_version}-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling#building-a-native-executable . \ No newline at end of file diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/README.maven.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/README.maven.ftl new file mode 100644 index 0000000000000..e751e91dd2b4b --- /dev/null +++ b/devtools/platform-descriptor-json/src/main/resources/templates/README.maven.ftl @@ -0,0 +1,30 @@ +# ${project_artifactId} project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +``` +./mvnw quarkus:dev +``` + +## Packaging and running the application + +The application is packageable using `./mvnw package`. +It produces the executable `${project_artifactId}-${project_version}-runner.jar` file in `/target` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/lib` directory. + +The application is now runnable using `java -jar target/${project_artifactId}-${project_version}-runner.jar`. + +## Creating a native executable + +You can create a native executable using: `./mvnw package -Pnative`. + +Or you can use Docker to build the native executable using: `./mvnw package -Pnative -Dquarkus.native.container-build=true`. + +You can then execute your binary: `./target/${project_artifactId}-${project_version}-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/building-native-image-guide . \ No newline at end of file diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/build.gradle-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/build.gradle-template.ftl index baf04e5c7e4e4..1cf1fab00efa4 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/build.gradle-template.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/build.gradle-template.ftl @@ -1,20 +1,8 @@ -// this block is necessary to make enforcedPlatform work for Quarkus plugin available -// only locally (snapshot) that is also importing the Quarkus BOM -buildscript { - repositories { - mavenLocal() - } - dependencies { - classpath "io.quarkus:quarkus-gradle-plugin:${quarkusPluginVersion}" - } -} - plugins { id 'java' + id 'io.quarkus' } -apply plugin: 'io.quarkus' - repositories { mavenLocal() mavenCentral() @@ -26,9 +14,6 @@ dependencies { testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' - - nativeTestImplementation 'io.quarkus:quarkus-junit5' - nativeTestImplementation 'io.rest-assured:rest-assured' } group '${project_groupId}' diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/settings.gradle-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/settings.gradle-template.ftl index 6dd13f2639657..fcc7b839cf47a 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/settings.gradle-template.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/java/settings.gradle-template.ftl @@ -4,13 +4,8 @@ pluginManagement { mavenCentral() gradlePluginPortal() } - resolutionStrategy { - eachPlugin { - if (requested.id.id == 'io.quarkus') { - useModule("io.quarkus:quarkus-gradle-plugin:${quarkus_version}") - } - } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" } } - rootProject.name='${project_artifactId}' diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/build.gradle-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/build.gradle-template.ftl index 4386c38fc3c2f..f604d4391c0da 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/build.gradle-template.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/build.gradle-template.ftl @@ -1,21 +1,9 @@ -// this block is necessary to make enforcedPlatform work for Quarkus plugin available -// only locally (snapshot) that is also importing the Quarkus BOM -buildscript { - repositories { - mavenLocal() - } - dependencies { - classpath "io.quarkus:quarkus-gradle-plugin:${quarkusPluginVersion}" - } -} - plugins { id 'org.jetbrains.kotlin.jvm' version "${kotlin_version}" id "org.jetbrains.kotlin.plugin.allopen" version "${kotlin_version}" + id 'io.quarkus' } -apply plugin: 'io.quarkus' - repositories { mavenLocal() mavenCentral() @@ -28,9 +16,6 @@ dependencies { testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' - - nativeTestImplementation 'io.quarkus:quarkus-junit5' - nativeTestImplementation 'io.rest-assured:rest-assured' } group '${project_groupId}' @@ -57,6 +42,7 @@ java { compileKotlin { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8 + kotlinOptions.javaParameters = true } compileTestKotlin { diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/pom.xml-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/pom.xml-template.ftl index 05aab87c9ea2f..a20ef9c175dec 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/pom.xml-template.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/pom.xml-template.ftl @@ -106,6 +106,7 @@ + true all-open @@ -114,6 +115,8 @@ + + diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/settings.gradle-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/settings.gradle-template.ftl index 6dd13f2639657..fcc7b839cf47a 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/settings.gradle-template.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/settings.gradle-template.ftl @@ -4,13 +4,8 @@ pluginManagement { mavenCentral() gradlePluginPortal() } - resolutionStrategy { - eachPlugin { - if (requested.id.id == 'io.quarkus') { - useModule("io.quarkus:quarkus-gradle-plugin:${quarkus_version}") - } - } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" } } - rootProject.name='${project_artifactId}' diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/spring-controller-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/spring-controller-template.ftl new file mode 100644 index 0000000000000..82d56d6c695d3 --- /dev/null +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/kotlin/spring-controller-template.ftl @@ -0,0 +1,15 @@ +package ${package_name}; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PathVariable; + + +@RestController +@RequestMapping("${path}") +class ${class_name} { + + @GetMapping + fun hello() = "hello" +} diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/build.gradle-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/build.gradle-template.ftl index 43140ec6b6f14..9603752b64751 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/build.gradle-template.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/build.gradle-template.ftl @@ -1,20 +1,8 @@ -// this block is necessary to make enforcedPlatform work for Quarkus plugin available -// only locally (snapshot) that is also importing the Quarkus BOM -buildscript { - repositories { - mavenLocal() - } - dependencies { - classpath "io.quarkus:quarkus-gradle-plugin:${quarkusPluginVersion}" - } -} - plugins { id 'scala' + id 'io.quarkus' } -apply plugin: 'io.quarkus' - repositories { mavenLocal() mavenCentral() @@ -22,13 +10,11 @@ repositories { dependencies { implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'org.scala-lang:scala-library:${scala_version}' implementation 'io.quarkus:quarkus-resteasy' testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' - - nativeTestImplementation 'io.quarkus:quarkus-junit5' - nativeTestImplementation 'io.rest-assured:rest-assured' } group '${project_groupId}' diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/settings.gradle-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/settings.gradle-template.ftl index 6dd13f2639657..fcc7b839cf47a 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/settings.gradle-template.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/settings.gradle-template.ftl @@ -4,13 +4,8 @@ pluginManagement { mavenCentral() gradlePluginPortal() } - resolutionStrategy { - eachPlugin { - if (requested.id.id == 'io.quarkus') { - useModule("io.quarkus:quarkus-gradle-plugin:${quarkus_version}") - } - } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" } } - rootProject.name='${project_artifactId}' diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/spring-controller-template.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/spring-controller-template.ftl new file mode 100644 index 0000000000000..2abac87be5b77 --- /dev/null +++ b/devtools/platform-descriptor-json/src/main/resources/templates/basic-rest/scala/spring-controller-template.ftl @@ -0,0 +1,12 @@ +package ${package_name}; + +import org.springframework.web.bind.annotation.{GetMapping, RequestMapping, RestController, PathVariable}; + + +@RestController +@RequestMapping(Array[String]("${path}")) +class ${class_name} { + + @GetMapping + def hello() = "hello" +} diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-jvm.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-jvm.ftl index c36cbd78d8652..5f8ef29672e3b 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-jvm.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-jvm.ftl @@ -14,18 +14,34 @@ # docker run -i --rm -p 8080:8080 quarkus/${project_artifactId}-jvm # ### -FROM fabric8/java-alpine-openjdk8-jre +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 + +ARG JAVA_PACKAGE=java-1.8.0-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.5 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install openssl curl ca-certificates ${JAVA_PACKAGE} \ + && microdnf update \ + && microdnf clean all \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 540 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV AB_ENABLED=jmx_exporter + COPY ${build_dir}/lib/* /deployments/lib/ COPY ${build_dir}/*-runner.jar /deployments/app.jar -EXPOSE 8080 -# run with user 1001 and be prepared for be running in OpenShift too -RUN adduser -G root --no-create-home --disabled-password 1001 \ - && chown -R 1001 /deployments \ - && chmod -R "g+rwX" /deployments \ - && chown -R 1001:root /deployments +EXPOSE 8080 USER 1001 -ENTRYPOINT [ "/deployments/run-java.sh" ] \ No newline at end of file +ENTRYPOINT [ "/deployments/run-java.sh" ] diff --git a/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-native.ftl b/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-native.ftl index 067411f7029f9..68263bfb605d3 100644 --- a/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-native.ftl +++ b/devtools/platform-descriptor-json/src/main/resources/templates/dockerfile-native.ftl @@ -14,9 +14,9 @@ # docker run -i --rm -p 8080:8080 quarkus/${project_artifactId} # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 WORKDIR /work/ COPY ${build_dir}/*-runner /work/application -RUN chmod 775 /work +RUN chmod 775 /work /work/application EXPOSE 8080 CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/docs/assembly-pdf.xml b/docs/assembly-pdf.xml new file mode 100644 index 0000000000000..93b55f31a1a65 --- /dev/null +++ b/docs/assembly-pdf.xml @@ -0,0 +1,28 @@ + + + quarkus-documentation-pdf + + zip + + true + + + target/generated-docs-pdf + + *.pdf + + + 0-glossary.pdf + attributes.pdf + duration-format-note.pdf + faq.pdf + index.pdf + quarkus-intro.pdf + README.pdf + status-include.pdf + + + + + diff --git a/docs/assembly.xml b/docs/assembly.xml index b852c1508f91a..cd1e68925bb01 100644 --- a/docs/assembly.xml +++ b/docs/assembly.xml @@ -1,7 +1,7 @@ - quarkus-docs + quarkus-documentation zip tar.gz diff --git a/docs/pom.xml b/docs/pom.xml index cc265d4af1ef0..b734f020a7cbd 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -15,8 +15,9 @@ jar - 2.0.0 + 2.2.0 2.0.0-RC.1 + 1.5.0-beta.8 https://quarkus.io https://github.com/quarkusio/quarkus https://github.com/quarkusio/quarkus-quickstarts @@ -90,13 +91,25 @@ WARN + src/main/asciidoc + true ${project.basedir}/../target/asciidoc/generated + ./images + font + true + + + - + true + + true ${project.version} ${version.surefire.plugin} ${graal-sdk.version-for-documentation} ${rest-assured.version} + ${keycloak.docker.image} ${quarkus-home-url} @@ -151,21 +164,11 @@ ${skipDocs} html5 - src/main/asciidoc - true coderay - ./images - font - true - - - - - true true true - true @@ -193,7 +196,7 @@ assembly.xml true - quarkus-docs-${project.version} + quarkus-documentation-${project.version} false target/ target/assembly/work @@ -220,4 +223,76 @@ + + + + documentation-pdf + + + documentation-pdf + + + + + + org.asciidoctor + asciidoctor-maven-plugin + + + org.asciidoctor + asciidoctorj-pdf + ${asciidoctorj-pdf.version} + + + + + output-pdf + process-resources + + process-asciidoc + + + ${skipDocs} + pdf + ${project.build.directory}/generated-docs-pdf + coderay + + ${basedir}/src/main/resources/theme + quarkus + ${basedir}/src/main/resources/theme/fonts + fas + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + assemble-pdf + package + + single + + + + assembly-pdf.xml + + true + quarkus-documentation-pdf-${project.version} + false + target/ + target/assembly-pdf/work + gnu + + + + + + + + diff --git a/docs/src/main/asciidoc/0-glossary.adoc b/docs/src/main/asciidoc/0-glossary.adoc index 4ac2f83adeca6..259e309503e0c 100644 --- a/docs/src/main/asciidoc/0-glossary.adoc +++ b/docs/src/main/asciidoc/0-glossary.adoc @@ -5,9 +5,9 @@ include::./attributes.adoc[] This is a collection of preferred term in the documentation and website. Please stay within these terms for consistency. -* Live reload:: for our `quarkus:dev` capability -* GraalVM:: preferred term for the VM creating native executable. No space. -* Substrate VM:: non-preferred. Only if you want to clarify which part of GraalVM we use. +* Live coding:: for our `quarkus:dev` capability +* GraalVM native image:: preferred term for the VM creating native executable. No space. +* Substrate VM:: non-preferred. Exclude. * Native Executable:: the executable that is compiled to native 1s and 0s * Docker image:: for the actual `Dockerfile` definition and when the tool chain is involved * Container:: when we discuss Quarkus running in... containers diff --git a/docs/src/main/asciidoc/amazon-lambda-http.adoc b/docs/src/main/asciidoc/amazon-lambda-http.adoc index ada6bcab0b3ef..1a1215fa7488e 100644 --- a/docs/src/main/asciidoc/amazon-lambda-http.adoc +++ b/docs/src/main/asciidoc/amazon-lambda-http.adoc @@ -3,18 +3,20 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// -= Quarkus - Amazon Lambda with Resteasy, Undertow, or Vert.x Web  += Quarkus - Amazon Lambda with RESTEasy, Undertow, or Vert.x Web  +:extension-status: preview include::./attributes.adoc[] - -The `quarkus-amazon-lambda-http` extension allows you to write microservices with Resteasy (JAX-RS), +The `quarkus-amazon-lambda-http` extension allows you to write microservices with RESTEasy (JAX-RS), Undertow (servlet), or Vert.x Web and make these microservices deployable as an Amazon Lambda using https://aws.amazon.com/api-gateway/[Amazon's API Gateway] and https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html[Amazon's SAM framework]. You can deploy your Lambda as a pure Java jar, or you can compile your project to a native image and deploy that for a smaller memory footprint and startup time. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -183,14 +185,20 @@ It should give you something like the following output: The `OutputValue` attribute is the root URL for your lambda. Copy it to your browser and add `hello` at the end. +[NOTE] +Responses for binary types will be automatically encoded with base64. This is different than the behavior using +`quarkus:dev` which will return the raw bytes. Amazon's API has additional restrictions requiring the base64 encoding. +In general, client code will automatically handle this encoding but in certain custom situations, you should be aware +you may need to manually manage that encoding. + == Examine the POM -If you want to adapt an existing Resteasy, Undertow, or Vert.x Web project to Amazon Lambda, there's a couple +If you want to adapt an existing RESTEasy, Undertow, or Vert.x Web project to Amazon Lambda, there's a couple of things you need to do. Take a look at the generate example project to get an example of what you need to adapt. 1. Include the `quarkus-amazon-lambda-http` extension as a pom dependency 2. Configure Quarkus build an `uber-jar` -3. If you are doing a native GraalVM build, Amazon requires you to rename your executable to `bootstrap` and zip it up. Notice that the `pom.xml` uses the `maven-assemby-plugin` to perform this requirement. +3. If you are doing a native GraalVM build, Amazon requires you to rename your executable to `bootstrap` and zip it up. Notice that the `pom.xml` uses the `maven-assembly-plugin` to perform this requirement. == Examine sam.yaml diff --git a/docs/src/main/asciidoc/amazon-lambda.adoc b/docs/src/main/asciidoc/amazon-lambda.adoc index 3151a7ba6a959..07aeb40960de8 100644 --- a/docs/src/main/asciidoc/amazon-lambda.adoc +++ b/docs/src/main/asciidoc/amazon-lambda.adoc @@ -4,16 +4,18 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Amazon Lambda +:extension-status: preview include::./attributes.adoc[] - The `quarkus-amazon-lambda` extension allows you to use Quarkus to build your Amazon Lambdas. Your lambdas can use injection annotations from CDI or Spring and other Quarkus facilities as you need them. Quarkus lambdas can be deployed using the Amazon Java Runtime, or you can build a native executable and use Amazon's Custom Runtime if you want a smaller memory footprint and faster cold boot startup time. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: diff --git a/docs/src/main/asciidoc/amqp.adoc b/docs/src/main/asciidoc/amqp.adoc index c27f7aee2d635..54191201fe2b1 100644 --- a/docs/src/main/asciidoc/amqp.adoc +++ b/docs/src/main/asciidoc/amqp.adoc @@ -4,10 +4,14 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Using AMQP with Reactive Messaging +:extension-status: preview + include::./attributes.adoc[] This guide demonstrates how your Quarkus application can utilize MicroProfile Reactive Messaging to interact with AMQP. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -301,6 +305,42 @@ You can build the native executable with: ./mvnw package -Pnative ---- +== Imperative usage + +Sometimes you need to have an imperative way of sending messages. + +For example, if you need to send a message to a stream from inside a REST endpoint when receiving a POST request. +In this case, you cannot use `@Output` because your method has parameters. + +For this, you can use an `Emitter`. + +[source, java] +---- +import io.smallrye.reactive.messaging.annotations.Channel; +import io.smallrye.reactive.messaging.annotations.Emitter; + +import javax.inject.Inject; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Consumes; +import javax.ws.rs.core.MediaType; + +@Path("/prices") +public class PriceResource { + + @Inject @Channel("price-create") Emitter priceEmitter; + + @POST + @Consumes(MediaType.TEXT_PLAIN) + public void addPrice(Double price) { + priceEmitter.send(price); + } +} +---- + +NOTE: The `Emitter` configuration is done the same way as the other stream configuration used by `@Incoming` and `@Outgoing`. +In addition, you can use `@OnOverflow` to configure a back-pressure strategy. + == Going further This guide has shown how you can interact with AMQP using Quarkus. diff --git a/docs/src/main/asciidoc/azure-functions-http.adoc b/docs/src/main/asciidoc/azure-functions-http.adoc index 99bdc0cd69135..355141d11fe92 100644 --- a/docs/src/main/asciidoc/azure-functions-http.adoc +++ b/docs/src/main/asciidoc/azure-functions-http.adoc @@ -3,15 +3,18 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// -= Quarkus - Azure Functions (Serverless) with Resteasy, Undertow, or Vert.x Web += Quarkus - Azure Functions (Serverless) with RESTEasy, Undertow, or Vert.x Web +:extension-status: preview include::./attributes.adoc[] -The `quarkus-azure-functions-http` extension allows you to write microservices with Resteasy (JAX-RS), +The `quarkus-azure-functions-http` extension allows you to write microservices with RESTEasy (JAX-RS), Undertow (servlet), or Vert.x Web and make these microservices deployable to the Azure Functions runtime. One azure function deployment can represent any number of JAX-RS, servlet, or Vert.x Web endpoints. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -93,7 +96,7 @@ https://{appName}.azurewebsites.net/api/vertx/hello == Extension maven dependencies -The sample project includes the Resteasy, Undertow, and Vert.x Web extensions. If you are only using one of those +The sample project includes the RESTEasy, Undertow, and Vert.x Web extensions. If you are only using one of those APIs (i.e. jax-rs only), respectively remove the maven dependency `quarkus-resteasy`, `quarkus-undertow`, and/or `quarkus-vertx-web`. diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 0a6336e6c1936..73ccb5df7fb49 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -37,7 +37,7 @@ What does having a working C developer environment mean? [source,shell] ---- # dnf (rpm-based) -sudo dnf install gcc glibc-devel zlib-devel +sudo dnf install gcc glibc-devel zlib-devel libstdc++-static # Debian-based distributions: sudo apt-get install build-essential libz-dev zlib1g-dev ---- @@ -102,6 +102,7 @@ export PATH=${GRAALVM_HOME}/bin:$PATH ==== GraalVM binaries are not (yet) notarized for macOS Catalina as reported in this https://github.com/oracle/graal/issues/1724[GraalVM issue]. This means that you may see the following error when using `gu`: +[source,shell] ---- “gu” cannot be opened because the developer cannot be verified ---- @@ -398,7 +399,7 @@ These are provided in `application.properties` the same as any other config prop The properties are shown below: -include::{generated-dir}/config/quarkus-core-native-config.adoc[opts=optional] +include::{generated-dir}/config/quarkus-native-pkg-native-config.adoc[opts=optional] == What's next? diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index bce767ca7c3d8..b60aee43e079e 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -11,10 +11,6 @@ include::./attributes.adoc[] :sectnumlevels: 4 :toc: -:numbered: -:sectnums: -:sectnumlevels: 4 - Quarkus DI solution is based on the http://docs.jboss.org/cdi/spec/2.0/cdi-spec.html[Contexts and Dependency Injection for Java 2.0, window="_blank"] specification. However, it is not a full CDI implementation verified by the TCK. Only a subset of the CDI features is implemented - see also <> and <>. @@ -55,7 +51,7 @@ To generate the index just add the following to your `pom.xml`: org.jboss.jandex jandex-maven-plugin - 1.0.6 + 1.0.7 make-index @@ -155,7 +151,7 @@ public class CounterBean { * Interceptors ** Business method interceptors: `@AroundInvoke` ** Interceptors for lifecycle event callbacks: `@PostConstruct`, `@PreDestroy`, `@AroundConstruct` -* Events and observers, including asynchronous events +* Events and observer methods, including asynchronous events and transactional observer methods [[limitations]] == Limitations @@ -168,10 +164,104 @@ public class CounterBean { * `beans.xml` descriptor content is ignored * Passivation and passivating scopes are not supported * Interceptor methods on superclasses are not implemented yet -* `BEFORE_COMPLETION`, `AFTER_COMPLETION`, `AFTER_FAILURE` and `AFTER_SUCCESS` transactional observers are not implemented yet == Non-standard Features +=== Eager Instantiation of Beans + +[[lazy_by_default]] +==== Lazy By Default + +By default, CDI beans are created lazily, when needed. +What exactly "needed" means depends on the scope of a bean. + +* A *normal scoped bean* (`@ApplicationScoped`, `@RequestScoped`, etc.) is needed when a method is invoked upon an injected instance (contextual reference per the specification). ++ +In other words, injecting a normal scoped bean will not suffice because a _client proxy_ is injected instead of a contextual instance of the bean. + +* A *bean with a pseudo-scope* (`@Dependent` and `@Singleton` ) is created when injected. + +.Lazy Instantiation Example +[source,java] +---- +@Singleton // => pseudo-scope +class AmazingService { + String ping() { + return "amazing"; + } +} + +@ApplicationScoped // => normal scope +class CoolService { + String ping() { + return "cool"; + } +} + +@Path("/ping") +public class PingResource { + + @Inject + AmazingService s1; <1> + + @Inject + CoolService s2; <2> + + @GET + public String ping() { + return s1.ping() + s2.ping(); <3> + } +} +---- +<1> Injection triggers the instantiation of `AmazingService`. +<2> Injection itself does not result in the instantiation of `CoolService`. A client proxy is injected. +<3> The first invocation upon the injected proxy triggers the instantiation of `CoolService`. + +==== Startup Event + +However, if you really need to instantiate a bean eagerly you can: + +* Declare an observer of the `StartupEvent` - the scope of the bean does not matter in this case: ++ +[source,java] +---- +@ApplicationScoped +class CoolService { + void startup(@Observes StartupEvent event) { <1> + } +} +---- +<1> A `CoolService` is created during startup to service the observer method invocation. + +* Use the bean in an observer of the `StartupEvent` - normal scoped beans must be used as described in <>: ++ +[source,java] +---- +@Dependent +class MyBeanStarter { + + void startup(@Observes StartupEvent event, AmazingService amazing, CoolService cool) { <1> + cool.toString(); <2> + } +} +---- +<1> The `AmazingService` is created during injection. +<2> The `CoolService` is a normal scoped bean so we have to invoke a method upon the injected proxy to force the instantiation. + +NOTE: Quarkus users are encouraged to always prefer the `@Observes StartupEvent` to `@Initialized(ApplicationScoped.class)` as explained in the link:lifecycle[Application Initialization and Termination] guide. + +=== Request Context Lifecycle + +The request context is also active: + +* during notification of a synchronous observer method. + +The request context is destroyed: + +* after the observer notification completes for an event, if it was not already active when the notification started. + +NOTE: An event with qualifier `@Initialized(RequestScoped.class)` is fired when the request context is initialized for an observer notification. Moreover, the events with qualifiers `@BeforeDestroyed(RequestScoped.class)` and `@Destroyed(RequestScoped.class)` are fired when the request context is destroyed. + === Qualified Injected Fields In CDI, if you declare a field injection point you need to use `@Inject` and optionally a set of qualifiers: @@ -379,8 +469,8 @@ NOTE: A bean registration that is a result of an `AdditionalBeanBuildItem` is re === Synthetic Beans Sometimes it is very useful to register a synthetic bean, i.e. a bean that doesn't need to have a corresponding java class. -In CDI this could be achieved using `AfterBeanDiscovery.addBean()` methods. -In Quarkus we produce a `BeanRegistrarBuildItem` and leverage the `io.quarkus.arc.processor.BeanConfigurator` API to build a synthetic bean definition. +In CDI, this could be achieved using `AfterBeanDiscovery.addBean()` methods. +In Quarkus, we produce a `BeanRegistrarBuildItem` and leverage the `io.quarkus.arc.processor.BeanConfigurator` API to build a synthetic bean definition. [source,java] ---- @@ -398,6 +488,8 @@ BeanRegistrarBuildItem syntheticBean() { NOTE: The output of a `BeanConfigurator` is recorded as bytecode. Therefore there are some limitations in how a synthetic bean instance is created. See also `BeanConfigurator.creator()` methods. +TIP: You can easily filter all class-based beans via the convenient `BeanStream` returned from the `RegistrationContext.beans()` method. + If an extension needs to produce other build items during the "bean registration" phase it should use the `BeanRegistrationPhaseBuildItem` instead. The reason is that injected objects are only valid during a `@BuildStep` method invocation. @@ -473,17 +565,18 @@ The following sample shows how to apply transformation to injection points with [source,java] ---- @BuildStep -InjectionPointTransformerBuildItem transform() { +InjectionPointTransformerBuildItem transformer() { return new InjectionPointTransformerBuildItem(new InjectionPointsTransformer() { public boolean appliesTo(Type requiredType) { - return requiredType.equals(Type.create(DotName.createSimple(Foo.class.getName()), Type.Kind.CLASS)); + return requiredType.name().equals(DotName.createSimple(Foo.class.getName())); } - public void transform(TransformationContext transformationContext) { - if (transformationContext.getQualifiers().stream() + public void transform(TransformationContext context) { + if (context.getQualifiers().stream() .anyMatch(a -> a.name().equals(DotName.createSimple(MyQualifier.class.getName())))) { - transformationContext.transform().removeAll() + context.transform() + .removeAll() .add(DotName.createSimple(MyOtherQualifier.class.getName())) .done(); } @@ -492,6 +585,35 @@ InjectionPointTransformerBuildItem transform() { } ---- +=== Observer Transformation + +Any https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#observer_methods[observer method] definition can be vetoed or transformed using an `ObserverTransformerBuildItem`. +The attributes that can be transformed include: + +- https://docs.jboss.org/cdi/api/2.0/javax/enterprise/inject/spi/ObserverMethod.html#getObservedQualifiers--[qualifiers] +- https://docs.jboss.org/cdi/api/2.0/javax/enterprise/inject/spi/ObserverMethod.html#getReception--[reception] +- https://docs.jboss.org/cdi/api/2.0/javax/enterprise/inject/spi/ObserverMethod.html#getPriority--[priority] +- https://docs.jboss.org/cdi/api/2.0/javax/enterprise/inject/spi/ObserverMethod.html#getTransactionPhase--[transaction phase] +- https://docs.jboss.org/cdi/api/2.0/javax/enterprise/inject/spi/ObserverMethod.html#isAsync--[asynchronous] + +[source,java] +---- +@BuildStep +ObserverTransformerBuildItem transformer() { + return new ObserverTransformerBuildItem(new ObserverTransformer() { + + public boolean appliesTo(Type observedType, Set qualifiers) { + return observedType.name.equals(DotName.createSimple(MyEvent.class.getName())); + } + + public void transform(TransformationContext context) { + // Veto all observers of MyEvent + context.veto(); + } + }); +} +---- + === Bean Deployment Validation Once the bean deployment is ready an extension can perform additional validations and inspect the found beans, observers and injection points. @@ -511,7 +633,7 @@ BeanDeploymentValidatorBuildItem beanDeploymentValidator() { } ---- -NOTE: See also `io.quarkus.arc.processor.BuildExtension.Key` to discover the available metadata. +TIP: You can easily filter all registered beans via the convenient `BeanStream` returned from the `ValidationContext.beans()` method. If an extension needs to produce other build items during the "validation" phase it should use the `ValidationPhaseBuildItem` instead. The reason is that injected objects are only valid during a `@BuildStep` method invocation. @@ -574,13 +696,15 @@ The built-in keys located in `io.quarkus.arc.processor.BuildExtension.Key` are: * `ANNOTATION_STORE` ** Contains an `AnnotationStore` that keeps information about all `AnnotationTarget` annotations after application of annotation transformers * `INJECTION_POINTS` -** `List` containing all injection points +** `Collection` containing all injection points * `BEANS` -** `List` containing all beans +** `Collection` containing all beans +* `REMOVED_BEANS` +** `Collection` containing all the removed beans; see <> for more information * `OBSERVERS` -** `List` containing all observers +** `Collection` containing all observers * `SCOPES` -** `List` containing all scopes, including custom ones +** `Collection` containing all scopes, including custom ones * `QUALIFIERS` ** `Map` containing all qualifiers * `INTERCEPTOR_BINDINGS` @@ -599,8 +723,10 @@ Here is a summary of which extensions can access which metadata: ** Has access to `ANNOTATION_STORE` * `InjectionPointsTransformer` ** Has access to `ANNOTATION_STORE`, `QUALIFIERS`, `INTERCEPTOR_BINDINGS`, `STEREOTYPES` +* `ObserverTransformer` +** Has access to `ANNOTATION_STORE`, `QUALIFIERS`, `INTERCEPTOR_BINDINGS`, `STEREOTYPES` * `BeanRegistrar` -** Has access to all build metadata +** Has access to `ANNOTATION_STORE`, `QUALIFIERS`, `INTERCEPTOR_BINDINGS`, `STEREOTYPES`, `BEANS` * `BeanDeploymentValidator` ** Has access to all build metadata diff --git a/docs/src/main/asciidoc/centralized-log-management.adoc b/docs/src/main/asciidoc/centralized-log-management.adoc new file mode 100644 index 0000000000000..50c97c66ed1e0 --- /dev/null +++ b/docs/src/main/asciidoc/centralized-log-management.adoc @@ -0,0 +1,398 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Centralized log management (Graylog, Logstash, Fluentd) + +include::./attributes.adoc[] +:es-version: 6.8.2 + +This guide explains how you can send your logs to a centralized log management system like Graylog, Logstash (inside the Elastic Stack or ELK - Elasticsearch, Logstash, Kibana) or +Fluentd (inside EFK - Elasticsearch, Fluentd, Kibana). + +There are a lot of different ways to centralize your logs (if you are using Kubernetes, the simplest way is to log to the console and ask you cluster administrator to integrate a central log manager inside your cluster). +In this guide, we will expose how to send them to an external tool using the `quarkus-logging-gelf` extension that can use TCP or UDP to send logs in the Graylog Extended Log Format (GELF). + +The `quarkus-logging-gelf` extension will add a GELF log handler to the underlying logging backend that Quarkus uses (jboss-logmanager). +By default, it is disabled, if you enable it but still use another handler (by default the console handler is enabled), your logs will be sent to both handlers. + +== Example application + +The following examples will all be based on the same example application that you can create with the following steps: + +- Create an application with the `quarkus-logging-gelf` extension. You can use the following Maven command to create it: + +[source,shell,subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=gelf-logging \ + -DclassName="org.acme.quickstart.GelfLoggingResource" \ + -Dpath="/gelf-logging" \ + -Dextensions="logging-gelf" +---- + +- For demonstration purposes, we create an endpoint that does nothing but log a sentence. You don't need to do this inside your application. + +[source,java] +---- +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.logging.Logger; + +@Path("/gelf-logging") +@ApplicationScoped +public class GelfLoggingResource { + private static final Logger LOG = Logger.getLogger(GelfLoggingResource.class); + + @GET + public void log() { + LOG.info("Some useful log message"); + } + +} +---- + +- Configure the GELF log handler to send logs to an external UDP endpoint on the port 12201: + +[source,properties] +---- +quarkus.log.handler.gelf.enabled=true +quarkus.log.handler.gelf.host=localhost +quarkus.log.handler.gelf.port=12201 +---- + +== Send logs to Graylog + +To send logs to Graylog, you first need to launch the components that compose the Graylog stack: + +- MongoDB +- Elasticsearch +- Graylog + +You can do this via the following docker-compose file that you can launch via `docker-compose run -d`: + +[source,yaml,subs="attributes"] +---- +version: '3.2' + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch-oss:{es-version} + ports: + - "9200:9200" + environment: + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + networks: + - graylog + + mongo: + image: mongo:4.0 + networks: + - graylog + + graylog: + image: graylog/graylog:3.1 + ports: + - "9000:9000" + - "12201:12201/udp" + - "1514:1514" + environment: + GRAYLOG_HTTP_EXTERNAL_URI: "http://127.0.0.1:9000/" + networks: + - graylog + depends_on: + - elasticsearch + - mongo + +networks: + graylog: + driver: bridge +---- + +Then, you need to create a UDP input in Graylog. +You can do it from the Graylog web console (System -> Input -> Select GELF UDP) available at http://localhost:9000 or via the API. + +This curl example will create a new Input of type GELF UDP, it uses the default login from Graylog (admin/admin). + +[source,shell] +---- +curl -H "Content-Type: application/json" -H "Authorization: Basic YWRtaW46YWRtaW4=" -H "X-Requested-By: curl" -X POST -v -d \ +'{"title":"udp input","configuration":{"recv_buffer_size":262144,"bind_address":"0.0.0.0","port":12201,"decompress_size_limit":8388608},"type":"org.graylog2.inputs.gelf.udp.GELFUDPInput","global":true}' \ +http://localhost:9000/api/system/inputs +---- + +Launch your application, you should see your logs arriving inside Graylog. + +== Send logs to Logstash / the Elastic Stack (ELK) + +Logstash comes by default with an Input plugin that can understand the GELF format, we will first create a pipeline that enables this plugin. + +Create the following file in `$HOME/pipelines/gelf.conf`: + +[source] +---- +input { + gelf { + port => 12201 + } +} +output { + stdout {} + elasticsearch { + hosts => ["http://elasticsearch:9200"] + } +} +---- + +Finally, launch the components that compose the Elastic Stack: + +- Elasticsearch +- Logstash +- Kibana + +You can do this via the following docker-compose file that you can launch via `docker-compose run -d`: + +[source,yaml,subs="attributes"] +---- +# Launch Elasticsearch +version: '3.2' + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch-oss:{es-version} + ports: + - "9200:9200" + - "9300:9300" + environment: + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + networks: + - elk + + logstash: + image: docker.elastic.co/logstash/logstash-oss:{es-version} + volumes: + - source: $HOME/pipelines + target: /usr/share/logstash/pipeline + type: bind + ports: + - "12201:12201/udp" + - "5000:5000" + - "9600:9600" + networks: + - elk + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana-oss:{es-version} + ports: + - "5601:5601" + networks: + - elk + depends_on: + - elasticsearch + +networks: + elk: + driver: bridge + +---- + +Launch your application, you should see your logs arriving inside the Elastic Stack; you can use Kibana available at http://localhost:5601/ to access them. + +== Send logs to Fluentd (EFK) + +First, you need to create a Fluentd image with the needed plugins: elasticsearch and input-gelf. +You can use the following Dockerfile that should be created inside a `fluentd` directory. + +[source] +---- +FROM fluent/fluentd:v1.3-debian +RUN ["gem", "install", "fluent-plugin-elasticsearch", "--version", "3.7.0"] +RUN ["gem", "install", "fluent-plugin-input-gelf", "--version", "0.3.1"] +---- + +You can build the image or let docker-compose build it for you. + +Then you need to create a fluentd configuration file inside `$HOME/fluentd/fluent.conf` + +[source] +---- + + type gelf + tag example.gelf + bind 0.0.0.0 + port 12201 + + + + @type elasticsearch + host elasticsearch + port 9200 + logstash_format true + +---- + +Finally, launch the components that compose the EFK Stack: + +- Elasticsearch +- Fluentd +- Kibana + +You can do this via the following docker-compose file that you can launch via `docker-compose run -d`: + +[source,yaml,subs="attributes"] +---- +version: '3.2' + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch-oss:{es-version} + ports: + - "9200:9200" + - "9300:9300" + environment: + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + networks: + - efk + + fluentd: + build: fluentd + ports: + - "12201:12201/udp" + volumes: + - source: $HOME/fluentd + target: /fluentd/etc + type: bind + networks: + - efk + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana-oss:{es-version} + ports: + - "5601:5601" + networks: + - efk + depends_on: + - elasticsearch + +networks: + efk: + driver: bridge +---- + +Launch your application, you should see your logs arriving inside EFK: you can use Kibana available at http://localhost:5601/ to access them. + +== Fluentd alternative: use Syslog + +You can also send your logs to Fluentd using a Syslog input. +As opposed to the GELF input, the Syslog input will not render multiline logs in one event, that's why we advise to use the GELF input that we implement in Quarkus. + +First, you need to create a Fluentd image with the elasticsearch plugin. +You can use the following Dockerfile that should be created inside a `fluentd` directory. + +[source] +---- +FROM fluent/fluentd:v1.3-debian +RUN ["gem", "install", "fluent-plugin-elasticsearch", "--version", "3.7.0"] +---- + +Then, you need to create a fluentd configuration file inside `$HOME/fluentd/fluent.conf` + +[source] +---- + + @type syslog + port 5140 + bind 0.0.0.0 + message_format rfc5424 + tag system + + + + @type elasticsearch + host elasticsearch + port 9200 + logstash_format true + +---- + +Then, launch the components that compose the EFK Stack: + +- Elasticsearch +- Fluentd +- Kibana + +You can do this via the following docker-compose file that you can launch via `docker-compose run -d`: + +[source,yaml,subs="attributes"] +---- +version: '3.2' + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch-oss:{es-version} + ports: + - "9200:9200" + - "9300:9300" + environment: + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + networks: + - efk + + fluentd: + build: fluentd + ports: + - "5140:5140/udp" + volumes: + - source: $HOME/fluentd + target: /fluentd/etc + type: bind + networks: + - efk + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana-oss:{es-version} + ports: + - "5601:5601" + networks: + - efk + depends_on: + - elasticsearch + +networks: + efk: + driver: bridge +---- + +Finally, configure your application to send logs to EFK using Syslog: + +[source,properties] +---- +quarkus.log.syslog.enable=true +quarkus.log.syslog.endpoint=localhost:5140 +quarkus.log.syslog.protocol=udp +quarkus.log.syslog.app-name=quarkus +quarkus.log.syslog.hostname=quarkus-test +---- + +Launch your application, you should see your logs arriving inside EFK: you can use Kibana available at http://localhost:5601/ to access them. + + +[[configuration-reference]] +== Configuration Reference + +Configuration is done through the usual `application.properties` file. + +include::{generated-dir}/config/quarkus-logging-gelf.adoc[opts=optional, leveloffset=+1] + +This extension uses the `logstash-gelf` library that allow more configuration options via system properties, +you can access its documentation here: https://logging.paluch.biz/ . diff --git a/docs/src/main/asciidoc/config.adoc b/docs/src/main/asciidoc/config.adoc index 11269265af0b3..4a525d22f9f30 100644 --- a/docs/src/main/asciidoc/config.adoc +++ b/docs/src/main/asciidoc/config.adoc @@ -396,7 +396,7 @@ _will also_ be taken into account. NOTE: Environment variables names are following the conversion rules of link:https://github.com/eclipse/microprofile-config/blob/master/spec/src/main/asciidoc/configsources.asciidoc#default-configsources[Eclipse MicroProfile] -NOTE: The `config/application.properties` features is available in development mode as well. To make use of it, `config/application.properties` needs to be placed inside the build tool's output directory (`target` for Maven and `build` for Gradle). +NOTE: The `config/application.properties` features is available in development mode as well. To make use of it, `config/application.properties` needs to be placed inside the build tool's output directory (`target` for Maven and `build/classes/java/main` for Gradle). Keep in mind however that any cleaning operation from the build tool like `mvn clean` or `gradle clean` will remove the `config` directory as well. === Configuration Profiles @@ -434,6 +434,22 @@ quarkus.http.port=9090 And then set the `QUARKUS_PROFILE` environment variable to `staging` to activate my profile. +[NOTE] +==== +The proper way to check the active profile programmatically is to use the `getActiveProfile` method of `io.quarkus.runtime.configuration.ProfileManager`. + +Using `@ConfigProperty("quarkus.profile")` will *not* work properly. +==== + +=== Clearing properties + +Run time properties which are optional, and which have had values set at build time or which have a default value, +may be explicitly cleared by assigning an empty string to the property. Note that this will _only_ affect +run time properties, and will _only_ work with properties whose values are not required. + +The property may be cleared by setting the corresponding `application.properties` property, setting the +corresponding system property, or setting the corresponding environment variable. + ==== Miscellaneous The default Quarkus application runtime profile is set to the profile used to build the application. For example: @@ -564,6 +580,89 @@ priority of 100. NOTE: This new converter also needs to be listed in a service file, i.e. `META-INF/services/org.eclipse.microprofile.config.spi.Converter`. +[[yaml]] +== YAML for Configuration + +=== Add YAML Config Support + +You might want to use YAML over properties for configuration. +Since link:https://github.com/smallrye/smallrye-config[SmallRye Config] brings support for YAML +configuration, Quarkus supports this as well. + +First you will need to add the YAML extension to your `pom.xml`: + +[source,xml] +---- + + io.quarkus + quarkus-config-yaml + +---- + +Or you can alternatively run this command in the directory containing your Quarkus project: + +[source,bash] +---- +./mvnw quarkus:add-extension -Dextensions="config-yaml" +---- + +Now Quarkus can read YAML configuration files. +The config directories and priorities are the same as before. + +NOTE: Quarkus will choose an `application.yaml` over an `application.properties`. +YAML files are just an alternative way to configure your application. +You should decide and keep one configuration type to avoid errors. + +==== Configuration Example +[source,yaml] +---- +# YAML supports comments +quarkus: + datasource: + url: jdbc:postgresql://localhost:5432/some-database + driver: org.postgresql.Driver + username: quarkus + password: quarkus +---- + +=== Profile dependent configuration + +Providing profile dependent configuration with YAML is done like with properties. +Just add the `%profile` wrapped in quotation marks before defining the key-value pairs: + +[source,yaml] +---- +"%dev": + quarkus: + datasource: + url: jdbc:postgresql://localhost:5432/some-database + driver: org.postgresql.Driver + username: quarkus + password: quarkus +---- + +=== Configuration key conflicts + +The MicroProfile Configuration specification defines configuration keys as an arbitrary `.`-delimited string. +However, structured formats like YAML naively only support a subset of the possible configuration namespace. +For example, consider the two configuration properties `quarkus.http.cors` and `quarkus.http.cors.methods`. +One property is the prefix of another, so it may not be immediately evident how to specify both keys in your YAML configuration. + +This is solved by using a null key (normally represented by `~`) for any YAML property which is a prefix of another one. Here's an example: + +.An example YAML configuration resolving prefix-related key name conflicts +[source,yaml] +---- +quarkus: + http: + cors: + ~: true + methods: GET,PUT,POST +---- + +In general, null YAML keys are not included in assembly of the configuration property name, allowing them to be used to +any level for disambiguating configuration keys. + == More info on how to configure Quarkus relies on Eclipse MicroProfile and inherits its features. diff --git a/docs/src/main/asciidoc/context-propagation.adoc b/docs/src/main/asciidoc/context-propagation.adoc index b18d8002a3989..39fe446e03569 100644 --- a/docs/src/main/asciidoc/context-propagation.adoc +++ b/docs/src/main/asciidoc/context-propagation.adoc @@ -18,7 +18,7 @@ If you write reactive/async code, you have to cut your work into a pipeline of c as well as `ThreadLocal` variables stop working, because your reactive code gets executed in another thread, after the caller ran its `finally` block. -link:https://github.com/eclipse/microprofile-context-propagation[Microprofile Context Propagation] was made to +link:https://github.com/eclipse/microprofile-context-propagation[MicroProfile Context Propagation] was made to make those Quarkus extensions work properly in reactive/async settings. It works by capturing those contextual values that used to be in thread-locals, and restoring them when your code is called. diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 46653fb491a9b..f9e8a7f8e2aef 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -265,6 +265,24 @@ If you have multiple datasources, all datasources will be checked and the status This behavior can be disabled via the property `quarkus.datasource.health.enabled`. +== Datasource Metrics + +If you are using the `quarkus-smallrye-metrics` extension, `quarkus-agroal` can expose some data source metrics on the +`/metrics` endpoint. This can be turned on by setting the property `quarkus.datasource.metrics.enabled` to true. + +For the exposed metrics to contain any actual values, it is necessary that metric collection is enabled internally +by Agroal mechanisms. By default, this metric collection mechanism gets turned on for all data sources if the `quarkus-smallrye-metrics` +is present and metrics for the Agroal extension are enabled. If you want to disable metrics for a particular data source, +this can be done by setting `quarkus.datasource.enable-metrics` to `false` (or `quarkus.datasource..enable-metrics` for a named datasource). This disables +collecting the metrics as well as exposing them in the `/metrics` endpoint, because it does not make sense to +expose metrics if the mechanism to collect them is disabled. + +Conversely, setting `quarkus.datasource.enable-metrics` to `true` (or `quarkus.datasource..enable-metrics` for a named datasource) explicitly can be used to enable collection of metrics even if +the `quarkus-smallrye-metrics` extension is not in use. This can be useful if you need to access the collected metrics programmatically. +They are available after calling `dataSource.getMetrics()` on an injected `AgroalDataSource` instance. If collection of metrics is disabled +for this data source, all values will be zero. + + == Narayana Transaction Manager integration If the Narayana JTA extension is also available, integration is automatic. @@ -319,4 +337,3 @@ quarkus.datasource.driver=org.h2.Driver == Agroal Configuration Reference include::{generated-dir}/config/quarkus-agroal.adoc[opts=optional, leveloffset=+1] - diff --git a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc new file mode 100644 index 0000000000000..5c28064d138fc --- /dev/null +++ b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc @@ -0,0 +1,109 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Deploying on Google cloud platform + +include::./attributes.adoc[] + +This guide covers: + +* Connecting to a CloudSQL postgresql instance with quarkus-agroal and service account using CloudRun + +== Prerequisites + +For this guide you need: + +* roughly 1 hour +* having access to an Google cloud platform project with owner rights. + +This guide will take as input an application developed in the link:datasource.adoc[datasource guide]. + +Make sure you have the application at hand working locally. + + +== Solution + +We recommend to follow the instructions in the next sections and build the application step by step. + +== Connecting to CloudSQL + +Google CloudSQL managed service allows 4 kinds of connection : + +. Using public IP +. Using private IP +. Using Cloud SQL Proxy +. Using service account + +The first two don't need anymore details, simply connect to you database following the link:datasource.adoc[datasource guide]. +The third allows you to connect as if locally, but is not available for AppEngine or CloudRun where you only deploy one artefact. +Please check link:https://cloud.google.com/sql/docs/postgres/external-connection-methods?hl=en[Connection options for external applications]. + +This guide will help you through the fourth possibility : connecting using service account. + +== Adding necessary dependencies to your application. + +You will need to add the _Cloud SQL Postgres Socket Factory_ and postgresql driver dependencies. + +With maven : +[source,xml, subs="attributes"] +---- + + + com.google.cloud.sql + postgres-socket-factory + 1.0.15 + + + + org.postgresql + postgresql + 42.2.8 + +---- + +With Gradle : +[source,groovy, subs="attributes"] +---- +// https://mvnrepository.com/artifact/com.google.cloud.sql/postgres-socket-factory +compile group: 'com.google.cloud.sql', name: 'postgres-socket-factory', version: '1.0.15' +// https://mvnrepository.com/artifact/org.postgresql/postgresql +compile group: 'org.postgresql', name: 'postgresql', version: '42.2.8' +---- + +== Configuring the CloudSQL instance + +If you haven't already please follow the link::https://cloud.google.com/sql/docs/postgres/create-instance[Creating instances guide]. + +Ensure you have a service account with _Cloud SQL Client_ role (minimum necessary role), and get your instance connection name (`PROJECT_ID:REGION:INSTANCE_ID`). + +== Configuring the application + +Configure your quarkus application as follows : + +[source,properties] +-- +quarkus.datasource.url=jdbc:postgresql:///${DB_NAME}?socketFactory=com.google.cloud.sql.postgres.SocketFactory&cloudSqlInstance=${CLOUD_SQL_INSTANCE} +quarkus.datasource.driver=org.postgresql.Driver +quarkus.datasource.username = ${DB_USER} +quarkus.datasource.password = ${DB_PASSWORD} +-- + +Prefere the use of environment variables that will allow you to connect to all your env depending on your runtime values + +WARNING: Don't forget to add your service account key to the deployed image, and to accept requests on the expected port + +[source,docker] +-- +FROM gcr.io/distroless/java:11 +COPY target/*.jar /app/application-runner.jar +COPY .json /app +WORKDIR /app +CMD ["application-runner.jar","-Dquarkus.http.host=0.0.0.0", "-Dquarkus.http.port=$PORT"] +-- + +NOTE: `$PORT` value will be injected automatically by CloudRun and is to be used for HealthCheck + + +You should now be able to connect to your instance. diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index a2aa697708c23..cadb4eebfd85c 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -93,5 +93,5 @@ Your application is accessible at the printed URL. This guide covered the deployment of a Quarkus application on Kubernetes and OpenShift. However, there is much more, and the integration with these environments has been tailored to make Quarkus applications execution very smooth. -For instance, the health extension can be used for health check; the configuration support allows mounting the application configuration using config map, the metric extension produces data _scrappable_ by Prometheus and so on. +For instance, the health extension can be used for health check; the configuration support allows mounting the application configuration using config map, the metric extension produces data _scrapable_ by Prometheus and so on. diff --git a/docs/src/main/asciidoc/deploying-to-openshift-s2i.adoc b/docs/src/main/asciidoc/deploying-to-openshift-s2i.adoc index 75d8d6a7c628e..0492c6a5c4a40 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift-s2i.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift-s2i.adoc @@ -90,8 +90,8 @@ The following command will create a chained build that is triggered whenever the oc new-build --name=minimal-quarkus-quickstart-native \ --docker-image=registry.access.redhat.com/ubi7-dev-preview/ubi-minimal \ --source-image=quarkus-quickstart-native \ - --source-image-path='/home/quarkus/quarkus-quickstart-1.0-SNAPSHOT-runner:.' \ - --dockerfile=$'FROM registry.access.redhat.com/ubi7-dev-preview/ubi-minimal:latest\nCOPY *-runner /application\nCMD /application\nEXPOSE 8080' \ + --source-image-path='/home/quarkus/application:.' \ + --dockerfile=$'FROM registry.access.redhat.com/ubi7-dev-preview/ubi-minimal:latest\nCOPY application /application\nCMD /application\nEXPOSE 8080' ---- To create a service from the minimal build run the following command: @@ -145,4 +145,4 @@ The `.s2i/environment` file in the quickstart sets required variables for the S2 This guide covered the deployment of a Quarkus application on OpenShift using S2I. However, there is much more, and the integration with these environments has been tailored to make Quarkus applications execution very smooth. -For instance, the health extension can be used for health check; the configuration support allows mounting the application configuration using config map, the metric extension produces data _scrappable_ by Prometheus and so on. +For instance, the health extension can be used for health check; the configuration support allows mounting the application configuration using config map, the metric extension produces data _scrapable_ by Prometheus and so on. diff --git a/docs/src/main/asciidoc/dynamodb.adoc b/docs/src/main/asciidoc/dynamodb.adoc index b91f14ef57b1d..68ed836d00f1a 100644 --- a/docs/src/main/asciidoc/dynamodb.adoc +++ b/docs/src/main/asciidoc/dynamodb.adoc @@ -5,6 +5,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Amazon DynamoDB Client +:extension-status: preview include::./attributes.adoc[] @@ -17,6 +18,8 @@ NOTE: The DynamoDB extension is based on https://docs.aws.amazon.com/sdk-for-jav It's a major rewrite of the 1.x code base that offers two programming models (Blocking & Async). Keep in mind it's actively developed and does not support yet all the features available in SDK 1.x such as https://github.com/aws/aws-sdk-java-v2/issues/36[Document APIs] or https://github.com/aws/aws-sdk-java-v2/issues/35[Object Mappers] +include::./status-include.adoc[] + The Quarkus extension supports two programming models: * Blocking access using URL Connection HTTP client (by default) or the Apache HTTP Client diff --git a/docs/src/main/asciidoc/flyway.adoc b/docs/src/main/asciidoc/flyway.adoc index e7822eee87ab9..08e2b4f113dc1 100644 --- a/docs/src/main/asciidoc/flyway.adoc +++ b/docs/src/main/asciidoc/flyway.adoc @@ -43,14 +43,24 @@ In your `pom.xml`, add the following dependencies: -- -Flyway support relies on the Quarkus default datasource config, you must add the default datasource properties -to the `{config-file}` file in order to allow Flyway to manage the schema. +Flyway support relies on the Quarkus datasource config. +It can be customized for the default datasource as well as for every <>. +First, you need to add the datasource config to the `{config-file}` file +in order to allow Flyway to manage the schema. Also, you can customize the Flyway behaviour by using the following properties: `quarkus.flyway.migrate-at-start`:: **true** to execute Flyway automatically when the application starts, **false** otherwise. + **default:** false +`quarkus.flyway.validate-on-migrate`:: +**true** to validate the applied migrations against the available ones, **false** otherwise. + +**default:** true + +`quarkus.flyway.clean-at-start`:: +**true** to execute Flyway clean command automatically when the application starts, **false** otherwise. + +**default:** false + `quarkus.flyway.locations`:: Comma-separated list of locations to scan recursively for migrations. The location type is determined by its prefix. Unprefixed locations or locations starting with classpath: point to a package on the classpath and may contain both SQL @@ -166,6 +176,52 @@ public class MigrationService { <1> Inject the Flyway object if you want to use it directly +== Multiple datasources + +Flyway can be configured for multiple datasources. +The Flyway properties are prefixed exactly the same way as the named datasources, for example: + +[source,properties] +-- +quarkus.datasource.driver=org.h2.Driver +quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:default +quarkus.datasource.username=username-default +quarkus.datasource.min-size=3 +quarkus.datasource.max-size=13 + +quarkus.datasource.users.driver=org.h2.Driver +quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:users +quarkus.datasource.users.username=username1 +quarkus.datasource.users.min-size=1 +quarkus.datasource.users.max-size=11 + +quarkus.datasource.inventory.driver=org.h2.Driver +quarkus.datasource.inventory.url=jdbc:h2:tcp://localhost/mem:inventory +quarkus.datasource.inventory.username=username2 +quarkus.datasource.inventory.min-size=2 +quarkus.datasource.inventory.max-size=12 + +# Flyway configuration for the default datasource +quarkus.flyway.schemas=DEFAULT_TEST_SCHEMA +quarkus.flyway.locations=db/default/location1,db/default/location2 +quarkus.flyway.migrate-at-start=true + +# Flyway configuration for the "users" datasource +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.migrate-at-start=true + +# Flyway configuration for the "inventory" datasource +quarkus.flyway.inventory.schemas=INVENTORY_TEST_SCHEMA +quarkus.flyway.inventory.locations=db/inventory/location1,db/inventory/location2 +quarkus.flyway.inventory.migrate-at-start=true +-- + +Notice there's an extra bit in the key. +The syntax is as follows: `quarkus.flyway.[optional name.][datasource property]`. + +NOTE: Without configuration, Flyway is set up for every datasource using the default settings. + == Using the Flyway object In case you are interested in using the `Flyway` object directly, you can inject it as follows: @@ -181,9 +237,17 @@ public class MigrationService { @Inject Flyway flyway; <1> + @Inject + @FlywayDataSource("inventory") <2> + Flyway flywayForInventory; + + @Inject + @Named("flyway_users") <3> + Flyway flywayForUsers; + public void checkMigration() { // Use the flyway instance manually - flyway.clean(); <2> + flyway.clean(); <4> flyway.migrate(); // This will print 1.0.0 System.out.println(flyway.info().current().getVersion().toString()); @@ -192,5 +256,7 @@ public class MigrationService { -- <1> Inject the Flyway object if you want to use it directly -<2> Use the Flyway instance directly +<2> Inject Flyway for named datasources using the Quarkus `FlywayDataSource` qualifier +<3> Inject Flyway for named datasources +<4> Use the Flyway instance directly diff --git a/docs/src/main/asciidoc/getting-started-knative.adoc b/docs/src/main/asciidoc/getting-started-knative.adoc index 98b4bbad61d89..2e54a67ae5bfe 100644 --- a/docs/src/main/asciidoc/getting-started-knative.adoc +++ b/docs/src/main/asciidoc/getting-started-knative.adoc @@ -144,5 +144,5 @@ curl -v -H 'Host: getting-started-knative.example.com' $IP_ADDRESS/hello/greetin This guide covered the deployment of a Quarkus application as Knative application on Kubernetes However, there is much more, and the integration with these environments has been tailored to make Quarkus applications execution very smooth. -For instance, the health extension can be used for health check; the configuration support allows mounting the application configuration using config map, the metric extension produces data _scrappable_ by Prometheus and so on. +For instance, the health extension can be used for health check; the configuration support allows mounting the application configuration using config map, the metric extension produces data _scrapable_ by Prometheus and so on. diff --git a/docs/src/main/asciidoc/getting-started.adoc b/docs/src/main/asciidoc/getting-started.adoc index fa1c7e9309d9d..af1c587a7b446 100644 --- a/docs/src/main/asciidoc/getting-started.adoc +++ b/docs/src/main/asciidoc/getting-started.adoc @@ -73,6 +73,8 @@ The solution is located in the `getting-started` directory. The easiest way to create a new Quarkus project is to open a terminal and run the following command: +For Linux & MacOS users + [source,shell,subs=attributes+] ---- mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ @@ -83,6 +85,22 @@ mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ cd getting-started ---- +For Windows users + +- If using cmd , (don't use forward slash `\`) + +[source,shell,subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create -DprojectGroupId=org.acme -DprojectArtifactId=getting-started -DclassName="org.acme.quickstart.GreetingResource" -Dpath="/hello" +---- + +- If using Powershell , wrap `-D` parameters in double quotes + +[source,shell,subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create "-DprojectGroupId=org.acme" "-DprojectArtifactId=getting-started" "-DclassName=org.acme.quickstart.GreetingResource" "-Dpath=/hello" +---- + It generates the following in `./getting-started`: * the Maven structure @@ -206,7 +224,7 @@ $ curl -w "\n" http://localhost:8080/hello hello ``` -Hit `CTRL+C` to stop the application, but you can also keep it running and enjoy the blazing fast hot-reload. +Hit `CTRL+C` to stop the application, or keep it running and enjoy the blazing fast hot-reload. [TIP] .Automatically add newline with `curl -w "\n"` @@ -446,7 +464,7 @@ The async version of the code is available in the {quickstarts-base-url}[GitHub] This guide covered the creation of an application using Quarkus. However, there is much more. -We recommend continuing the journey with the link:building-native-image[building a native executable guide], where you learn about the native executable creation and the packaging in a container. +We recommend continuing the journey with the link:building-native-image[building a native executable guide], where you learn about creating a native executable and packaging it in a container. In addition, the link:tooling[tooling guide] document explains how to: diff --git a/docs/src/main/asciidoc/gradle-config.adoc b/docs/src/main/asciidoc/gradle-config.adoc index f4a358b42bace..f82b083dd34dc 100644 --- a/docs/src/main/asciidoc/gradle-config.adoc +++ b/docs/src/main/asciidoc/gradle-config.adoc @@ -8,43 +8,42 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc include::./attributes.adoc[] // tag::repositories[] -Quarkus Gradle plugin is not yet published to the https://plugins.gradle.org/[Gradle Plugin Portal], -so you need to add the following at the top of your './settings.gradle' file: +The Quarkus Gradle plugin is published to the https://plugins.gradle.org/plugin/io.quarkus[Gradle Plugin Portal]. + +To use it, add the following to your `build.gradle` file: + [source, groovy, subs=attributes+] ---- -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } - resolutionStrategy { - eachPlugin { - if (requested.id.id == 'io.quarkus') { - useModule("io.quarkus:quarkus-gradle-plugin:${requested.version}") - } - } - } +plugins { + id 'java' + id 'io.quarkus' } ---- -Or, if you use the Gradle Kotlin DSL, you need to add the following at the top of your './settings.gradle.kts' file: -[source, kotlin, subs=attributes+] +You also need to add the following at the top of your `settings.gradle` file: +[source, groovy, subs=attributes+] ---- pluginManagement { repositories { mavenCentral() gradlePluginPortal() } - resolutionStrategy { - eachPlugin { - if (requested.id.id == "io.quarkus") { - useModule("io.quarkus:quarkus-gradle-plugin:${requested.version}") - } - } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" } } ---- -This won't be necessary anymore once the Quarkus Gradle plugin is published in the Gradle plugin portal. +NOTE:: the `plugins{}` method in `settings.gradle` is not supported in Gradle 5.x. In this case make sure to explicitly declare the plugin version in the `build.gradle` file like the example below: + +[source, groovy, subs=attributes+] +---- +plugins { + id 'java' + id 'io.quarkus' version '{quarkus-version}' +} +---- + + // end::repositories[] diff --git a/docs/src/main/asciidoc/gradle-tooling.adoc b/docs/src/main/asciidoc/gradle-tooling.adoc index cd49704f7b322..a7dd013d1e3de 100644 --- a/docs/src/main/asciidoc/gradle-tooling.adoc +++ b/docs/src/main/asciidoc/gradle-tooling.adoc @@ -98,7 +98,7 @@ or, if you use the Gradle Kotlin DSL: ---- tasks.test { systemProperty("quarkus.test.profile", "foo") <1> - } +} ---- <1> The `foo` configuration profile will be used to run the tests. @@ -233,9 +233,9 @@ In IntelliJ: In a separated terminal or in the embedded terminal, run `./gradlew quarkusDev`. Enjoy! -**Apache Netbeans** +**Apache NetBeans** -In Netbeans: +In NetBeans: 1. Select `File -> Open Project` 2. Select the project root @@ -309,6 +309,18 @@ The native executable would then be produced by executing: ./gradlew buildNative ---- + +== Running native tests + +Run the native tests using: + +[source,shell] +---- +./gradlew testNative +---- + +This task depends on `buildNative`, so it will generate the native image before running the tests. + == Building Uber-Jars Quarkus Gradle plugin supports the generation of Uber-Jars by specifying an `--uber-jar` argument as follows: @@ -329,8 +341,4 @@ The entries are relative to the root of the generated Uber-Jar. You can specify == Building with `./gradlew build` -By default, `./gradlew build` will build the native image also. To skip it, use -``` -./gradlew build -x buildNative -x testNative -``` - +Starting from 1.1.0.Final, `./gradlew build` will no longer build the native image. Use the `buildNative` task explicitly as explained above if needed. diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc index 68b35b1ce60f3..dbd992ed01540 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc @@ -161,6 +161,10 @@ List allPersons = Person.listAll(); // finding a specific person by ID person = Person.findById(personId); +// finding a specific person by ID via an Optional +Optional optional = Person.findByIdOptional(personId); +person = optional.orElseThrow(() -> new NotFoundException()); + // finding all living persons List livingPersons = Person.list("status", Status.Alive); @@ -175,6 +179,10 @@ Person.delete("status", Status.Alive); // delete all persons Person.deleteAll(); + +// update all living persons +Person.update("name = 'Moral' where status = ?1", Status.Alive); + ---- All `list` methods have equivalent `stream` versions. @@ -282,21 +290,27 @@ public class Person extends PanacheEntity { Normally, HQL queries are of this form: `from EntityName [where ...] [order by ...]`, with optional elements at the end. -If your query does not start with `from`, we support the following additional forms: +If your select query does not start with `from`, we support the following additional forms: - `order by ...` which will expand to `from EntityName order by ...` - `` (and single parameter) which will expand to `from EntityName where = ?` - `` will expand to `from EntityName where ` +If your update query does not start with `update`, we support the following additional forms: + +- `from EntityName ...` which will expand to `update from EntityName ...` +- `set? ` (and single parameter) which will expand to `update from EntityName set = ?` +- `set? ` will expand to `update from EntityName set ` + NOTE: You can also write your queries in plain link:https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#hql[HQL]: [source,java] ---- Order.find("select distinct o from Order o left join fetch o.lineItems"); +Order.update("update from Person set name = 'Moral' where status = ?", Status.Alive); ---- - == Query parameters You can pass query parameters by index (1-based) as shown below: diff --git a/docs/src/main/asciidoc/hibernate-search-elasticsearch.adoc b/docs/src/main/asciidoc/hibernate-search-elasticsearch.adoc index 0346279477063..8b8a085e55eb9 100644 --- a/docs/src/main/asciidoc/hibernate-search-elasticsearch.adoc +++ b/docs/src/main/asciidoc/hibernate-search-elasticsearch.adoc @@ -4,7 +4,7 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Hibernate Search guide - +:extension-status: preview include::./attributes.adoc[] You have a Hibernate ORM-based application? You want to provide a full-featured full-text search to your users? You're at the right place. @@ -12,12 +12,12 @@ You have a Hibernate ORM-based application? You want to provide a full-featured With this guide, you'll learn how to synchronize your entities to an Elasticsearch cluster in a heart beat with Hibernate Search. We will also explore how you can can query your Elasticsearch cluster using the Hibernate Search API. +include::./status-include.adoc[] + [WARNING] ==== -The version of Hibernate Search shipped with Quarkus is still a Beta. - -While APIs are quite stable and the code is of production quality and thoroughly tested, -some features are still missing, performance might not be optimal and some APIs might change before the final release. +This extension is based on a beta version of Hibernate Search. +While APIs are quite stable and the code is of production quality and thoroughly tested, some features are still missing, performance might not be optimal and some APIs or configuration properties might change as the extension matures. ==== == Prerequisites @@ -222,6 +222,9 @@ public class LibraryResource { book.title = title; book.author = author; book.persist(); + + author.books.add(book); + author.persist(); } @DELETE @@ -431,13 +434,13 @@ To fulfill our requirements, let's create the following implementation: ---- package org.acme.hibernate.search.elasticsearch.config; +import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurationContext; import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer; -import org.hibernate.search.backend.elasticsearch.analysis.model.dsl.ElasticsearchAnalysisDefinitionContainerContext; public class AnalysisConfigurer implements ElasticsearchAnalysisConfigurer { @Override - public void configure(ElasticsearchAnalysisDefinitionContainerContext context) { + public void configure(ElasticsearchAnalysisConfigurationContext context) { context.analyzer("name").custom() // <1> .tokenizer("standard") .tokenFilters("asciifolding", "lowercase"); @@ -594,7 +597,7 @@ Let's use Docker to start one of each: [source, shell] ---- -docker run -it --rm=true --name elasticsearch_quarkus_test -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.4.0 +docker run -it --rm=true --name elasticsearch_quarkus_test -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.5.0 ---- [source, shell] diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 94e6937b8565c..58a854518e0b4 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -41,12 +41,12 @@ want to use the HTTP root as this affects everything that Quarkus serves. If both are specified then all non-Servlet web endpoints will be relative to `quarkus.http.root-path`, while Servlet's will be served relative to `{quarkus.http.root-path}/{quarkus.servlet.context-path}`. -If Restassured is used for testing and `quarkus.http.root-path` is set then Quarkus will automatically configure the +If REST Assured is used for testing and `quarkus.http.root-path` is set then Quarkus will automatically configure the base URL for use in Quarkus tests, so test URL's should not include the root path. == Supporting secure connections with SSL -In order to have Undertow support secure connections, you must either provide a certificate and associated key file, or supply a keystore. +In order to have Quarkus support secure connections, you must either provide a certificate and associated key file, or supply a keystore. In both cases, a password must be provided. See the designated paragraph for a detailed description of how to provide it. @@ -153,8 +153,10 @@ quarkus.http.cors.exposed-headers=Content-Disposition quarkus.http.cors.access-control-max-age=24H ---- -== Http Limits Configuration +== HTTP Limits Configuration + The following properties are supported. + [cols=" ---- - === undertow-handlers.conf You can make use of the Undertow predicate language using an `undertow-handlers.conf` file. This file should be placed in the `META-INF` directory of your application jar. This file contains handlers defined using the link:http://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html#predicates-attributes-and-handlers[Undertow predicate language]. +=== Configuring HTTP Access Logs + +You can add HTTP request logging by configuring the `AccessHandler` in the `undertow-handlers.conf` file. + +The simplest possible configuration can be a standard Apache `common` Log Format: + +[source] +---- +access-log('common') +---- + +This will log every request using the standard Quarkus logging infrastructure under the `io.undertow.accesslog` category. + +You can customize the category like this: + +[source] +---- +access-log(format='common', category='my.own.category') +---- + +Finally the logging format can be customized: + +[source] +---- +access-log(format='%h %l %u %t "%r" %s %b %D "%{i,Referer}" "%{i,User-Agent}" "%{i,X-Request-ID}"', category='my.own.category') +---- + === web.xml If you are using a `web.xml` file as your configuration file, you can place it in the `src/main/resources/META-INF` directory. + diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc index 0ab71bddc98f6..56b6c557bff6c 100644 --- a/docs/src/main/asciidoc/index.adoc +++ b/docs/src/main/asciidoc/index.adoc @@ -17,9 +17,11 @@ include::quarkus-intro.adoc[tag=intro] * link:tooling.html[Use tooling with Quarkus] * link:config.html[Configuring Your Application] * link:logging.html[Configuring Logging] +* link:central-logging.html[Central log management] * link:lifecycle.html[Application Initialization and Termination] * link:rest-json.html[Writing JSON REST Services] * link:scheduler.html[Schedule Periodic Tasks] +* link:quartz.html[Schedule Periodic Tasks with Quartz] * link:websockets.html[Using Websockets] * link:validation.html[Validation with Hibernate Validator] * link:transaction.html[Using Transactions] diff --git a/docs/src/main/asciidoc/infinispan-embedded.adoc b/docs/src/main/asciidoc/infinispan-embedded.adoc index b22439d5d3b05..31392de1a41f3 100644 --- a/docs/src/main/asciidoc/infinispan-embedded.adoc +++ b/docs/src/main/asciidoc/infinispan-embedded.adoc @@ -4,6 +4,7 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Infinispan Embedded +:extension-status: preview include::./attributes.adoc[] @@ -13,6 +14,8 @@ directly in your application. Check out the link:https://infinispan.org/documentation/[Infinispan documentation] to find out more about the Infinispan project. +include::./status-include.adoc[] + == Adding the Infinispan Embedded Extension After you set up your Quarkus project, run the following command from the base directory: diff --git a/docs/src/main/asciidoc/javascript/config.js b/docs/src/main/asciidoc/javascript/config.js index 22bbd6187b13b..b3b6f603c04e1 100644 --- a/docs/src/main/asciidoc/javascript/config.js +++ b/docs/src/main/asciidoc/javascript/config.js @@ -13,7 +13,7 @@ if(tables){ if (table.classList.contains('searchable')) { // activate search engine only when needed var input = document.createElement("input"); input.setAttribute("type", "search"); - input.setAttribute("placeholder", "filter configuration"); + input.setAttribute("placeholder", "FILTER CONFIGURATION"); input.id = "config-search-"+(idx++); caption.children.item(0).appendChild(input); input.addEventListener("keyup", initiateSearch); diff --git a/docs/src/main/asciidoc/jms.adoc b/docs/src/main/asciidoc/jms.adoc index 93de0306d7f75..69a677c98fa51 100644 --- a/docs/src/main/asciidoc/jms.adoc +++ b/docs/src/main/asciidoc/jms.adoc @@ -5,9 +5,12 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Using Artemis JMS extension include::./attributes.adoc[] +:extension-status: preview This guide demonstrates how your Quarkus application can use Artemis JMS messaging. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: diff --git a/docs/src/main/asciidoc/kafka-streams.adoc b/docs/src/main/asciidoc/kafka-streams.adoc index 0bfca2d6d52c1..05bb966698d61 100644 --- a/docs/src/main/asciidoc/kafka-streams.adoc +++ b/docs/src/main/asciidoc/kafka-streams.adoc @@ -1112,6 +1112,107 @@ CMD ["./application", "-Dquarkus.http.host=0.0.0.0", "-Xmx32m"] Now start Docker Compose as described above (don't forget to rebuild the container images). +== Kafka Streams Health Checks + +If you are using the `quarkus-smallrye-health` extension, `quarkus-kafka-streams` will automatically add: + +* a readiness health check to validate that all topics declared in the `quarkus.kafka-streams.topics` property are created, +* a liveness health check based on the Kafka Streams state. + +So when you access the `/health` endpoint of your application you will have information about the state of the Kafka Streams and the available and/or missing topics. + +This is an example of when the status is `DOWN`: +[source, subs=attributes+] +---- +curl -i http://aggregator:8080/health + +HTTP/1.1 503 Service Unavailable +content-type: application/json; charset=UTF-8 +content-length: 454 + +{ + "status": "DOWN", + "checks": [ + { + "name": "Kafka Streams state health check", <1> + "status": "DOWN", + "data": { + "state": "CREATED" + } + }, + { + "name": "Kafka Streams topics health check", <2> + "status": "DOWN", + "data": { + "available_topics": "weather-stations,temperature-values", + "missing_topics": "hygrometry-values" + } + } + ] +} +---- +<1> Liveness health check. Also available at `/health/live` endpoint. +<2> Readiness health check. Also available at `/health/ready` endpoint. + +So as you can see, the status is `DOWN` as soon as one of the `quarkus.kafka-streams.topics` is missing or the Kafka Streams `state` is not `RUNNING`. + +If no topics are available, the `available_topics` key will not be present in the `data` field of the `Kafka Streams topics health check`. +As well as if no topics are missing, the `missing_topics` key will not be present in the `data` field of the `Kafka Streams topics health check`. + +You can of course disable the health check of the `quarkus-kafka-streams` extension by setting the `quarkus.kafka-streams.health.enabled` property to `false` in your `application.properties`. + +Obviously you can create your liveness and readiness probes based on the respective endpoints `/health/live` and `/health/ready`. + +=== Liveness health check + +Here is an example of the liveness check: +``` +curl -i http://aggregator:8080/health/live + +HTTP/1.1 503 Service Unavailable +content-type: application/json; charset=UTF-8 +content-length: 225 + +{ + "status": "DOWN", + "checks": [ + { + "name": "Kafka Streams state health check", + "status": "DOWN", + "data": { + "state": "CREATED" + } + } + ] +} +``` +The `state` is coming from the `KafkaStreams.State` enum. + +=== Readiness health check + +Here is an example of the readiness check: +``` +curl -i http://aggregator:8080/health/ready + +HTTP/1.1 503 Service Unavailable +content-type: application/json; charset=UTF-8 +content-length: 265 + +{ + "status": "DOWN", + "checks": [ + { + "name": "Kafka Streams topics health check", + "status": "DOWN", + "data": { + "missing_topics": "weather-stations,temperature-values" + } + } + ] +} + +``` + == Going Further This guide has shown how you can build stream processing applications using Quarkus and the Kafka Streams APIs, diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index d1eb5b7fcb1ab..d50be50dec3a2 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -308,6 +308,243 @@ You can build the native executable with: ./mvnw package -Pnative ---- +== Imperative usage + +Sometimes, you need to have an imperative way of sending messages. + +For example, if you need to send a message to a stream, from inside a REST endpoint, when receiving a POST request. +In this case, you cannot use `@Output` because your method has parameters. + +For this, you can use an `Emitter`. + +[source, java] +---- +import io.smallrye.reactive.messaging.annotations.Channel; +import io.smallrye.reactive.messaging.annotations.Emitter; + +import javax.inject.Inject; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Consumes; +import javax.ws.rs.core.MediaType; + +@Path("/prices") +public class PriceResource { + + @Inject @Channel("price-create") Emitter priceEmitter; + + @POST + @Consumes(MediaType.TEXT_PLAIN) + public void addPrice(Double price) { + priceEmitter.send(price); + } +} +---- + +NOTE: The `Emitter` configuration is done the same way as the other stream configuration used by `@Incoming` and `@Outgoing`. +In addition, you can use `@OnOverflow` to configure back-pressure strategy. + +== Kafka Health Check + +If you are using the `quarkus-smallrye-health` extension, `quarkus-kafka` can add a readiness health check +to validate the connection to the broker. This is disabled by default. + +If enabled, when you access the `/health/ready` endpoint of your application you will have information about the connection validation status. + +This behavior can be enabled by setting the `quarkus.kafka.health.enabled` property to `true` in your `application.properties`. +You also need to point `quarkus.kafka.bootstrap-servers` to your Kafka cluster. + +== JSON serialization + +Quarkus has built-in capabilities to deal with JSON Kafka messages. + +Imagine we have a `Fruit` pojo as follows: + +[source,java] +---- +public class Fruit { + + public String name; + public int price; + + public Fruit() { + } + + public Fruit(String name, int price) { + this.name = name; + this.price = price; + } +} +---- + +And we want to use it to receive messages from Kafka, make some price transformation, and send messages back to Kafka. + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.Broadcast; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import javax.enterprise.context.ApplicationScoped; + +/** +* A bean consuming data from the "fruit-in" Kafka topic and applying some price conversion. +* The result is pushed to the "fruit-out" stream. +*/ +@ApplicationScoped +public class FruitProcessor { + + private static final double CONVERSION_RATE = 0.88; + + @Incoming("fruit-in") + @Outgoing("fruit-out") + @Broadcast + public double process(Fruit fruit) { + fruit.price = fruit.price * CONVERSION_RATE; + return fruit; + } + +} +---- + +To do this, we will need to setup JSON serialization with JSON-B or Jackson. + +NOTE: With JSON serialization correctly configured, you can also use `Publisher` and `Emitter`. + +=== Serializing via JSON-B + +First, you need to include the `quarkus-jsonb` extension (if you already use the `quarkus-resteasy-jsonb` extension, this is not needed). + +[source, xml] +---- + + io.quarkus + quarkus-jsonb + +---- + +There is an existing `JsonbSerializer` that can be used to serialize all pojos via JSON-B, +but the corresponding deserializer is generic, so it needs to be subclassed. + +So, let's create a `FruitDeserializer` that extends the generic `JsonbDeserializer`. + +[source,java] +---- +package com.acme.fruit.jsonb; + +import io.quarkus.kafka.client.serialization.JsonbDeserializer; + +public class FruitDeserializer extends JsonbDeserializer { + public FruitDeserializer(){ + // pass the class to the parent. + super(Fruit.class); + } +} +---- + +NOTE: If you don't want to create a deserializer for each of your pojo, you can use the generic `io.vertx.kafka.client.serialization.JsonObjectDeserializer` +that will deserialize to a `javax.json.JsonObject`. The corresponding serializer can also be used: `io.vertx.kafka.client.serialization.JsonObjectSerializer`. + +Finally, configure your streams to use the JSON-B serializer and deserializer. + +[source,properties] +---- +# Configure the Kafka source (we read from it) +mp.messaging.incoming.fruit-in.connector=smallrye-kafka +mp.messaging.incoming.fruit-in.topic=fruit-in +mp.messaging.incoming.fruit-in.value.deserializer=com.acme.fruit.jsonb.FruitDeserializer + +# Configure the Kafka sink (we write to it) +mp.messaging.outgoing.fruit-out.connector=smallrye-kafka +mp.messaging.outgoing.fruit-out.topic=fruit-out +mp.messaging.outgoing.fruit-out.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer +---- + +Now, your Kafka messages will contain a JSON-B serialized representation of your Fruit pojo. + +=== Serializing via Jackson + +First, you need to include the `quarkus-jackson` extension (if you already use the `quarkus-jackson-jsonb` extension, this is not needed). + +[source, xml] +---- + + io.quarkus + quarkus-jackson + +---- + +There is an existing `ObjectMapperSerializer` that can be used to serialize all pojos via Jackson, +but the corresponding deserializer is generic, so it needs to be subclassed. + +So, let's create a `FruitDeserializer` that extends the `ObjectMapperDeserializer`. + +[source,java] +---- +package com.acme.fruit.jackson; + +import io.quarkus.kafka.client.serialization.ObjectMapperDeserializer; + +public class FruitDeserializer extends ObjectMapperDeserializer { + public FruitDeserializer(){ + // pass the class to the parent. + super(Fruit.class); + } +} +---- + +Finally, configure your streams to use the Jackson serializer and deserializer. + +[source,properties] +---- +# Configure the Kafka source (we read from it) +mp.messaging.incoming.fruit-in.connector=smallrye-kafka +mp.messaging.incoming.fruit-in.topic=fruit-in +mp.messaging.incoming.fruit-in.value.deserializer=com.acme.fruit.jackson.FruitDeserializer + +# Configure the Kafka sink (we write to it) +mp.messaging.outgoing.fruit-out.connector=smallrye-kafka +mp.messaging.outgoing.fruit-out.topic=fruit-out +mp.messaging.outgoing.fruit-out.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer +---- + +Now, your Kafka messages will contain a Jackson serialized representation of your Fruit pojo. + +=== Sending JSON Server-Sent Events (SSE) + +If you want RESTEasy to send JSON Server-Sent Events, you need to use the `@SseElementType` annotation to define the content type of the events, +as the method will be annotated with `@Produces(MediaType.SERVER_SENT_EVENTS)`. + +The following example shows how to use SSE from a Kafka topic source. + +[source,java] +---- +import io.smallrye.reactive.messaging.annotations.Channel; +import org.reactivestreams.Publisher; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.jboss.resteasy.annotations.SseElementType; + +@Path("/fruits") +public class PriceResource { + + @Inject + @Channel("fruit-out") Publisher fruits; + + @GET + @Path("/stream") + @Produces(MediaType.SERVER_SENT_EVENTS) + @SseElementType(MediaType.APPLICATION_JSON) + public Publisher stream() { + return fruits; + } +} +---- + == Going further This guide has shown how you can interact with Kafka using Quarkus. diff --git a/docs/src/main/asciidoc/kogito.adoc b/docs/src/main/asciidoc/kogito.adoc index e8526300214d9..ba5f1d67b26ae 100644 --- a/docs/src/main/asciidoc/kogito.adoc +++ b/docs/src/main/asciidoc/kogito.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc = Quarkus - Using Kogito to add business automation capabilities to an application include::./attributes.adoc[] +:extension-status: preview This guide demonstrates how your Quarkus application can use Kogito to add business automation to power it up with business processes and rules. @@ -15,6 +16,8 @@ Drools (for business rules) and jBPM (for business processes). Kogito aims at pr to business automation where the main message is to expose your business knowledge (processes, rules and decisions) in a domain specific way. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -390,6 +393,20 @@ curl -X GET http://localhost:8080/persons \ To learn more about persistence in Kogito visit https://github.com/kiegroup/kogito-runtimes/wiki/Persistence[this page] +== Using decision tables + +Kogito allows to define business rules as decision tables using the Microsoft Excel file formats. +To be able to use such assets in your application, an additional dependency is required: + +[source,xml] +---- + + org.kie.kogito + drools-decisiontables + +---- + +Once the dependency is added to the project, decision tables in `xls` or `xlsx` format can be properly handled. == References diff --git a/docs/src/main/asciidoc/kotlin.adoc b/docs/src/main/asciidoc/kotlin.adoc index ffea28298cd2d..d27de1d7c2883 100644 --- a/docs/src/main/asciidoc/kotlin.adoc +++ b/docs/src/main/asciidoc/kotlin.adoc @@ -5,12 +5,15 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Using Kotlin +:extension-status: preview include::./attributes.adoc[] https://kotlinlang.org/[Kotlin] is a very popular programming language that targets the JVM (amongst other environments). Kotlin has experienced a surge in popularity the last few years making it the most popular JVM language, except for Java of course. Quarkus provides first class support for using Kotlin as will be explained in this guide. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -34,7 +37,7 @@ mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ -DclassName="org.acme.rest.GreetingResource" \ -Dpath="/greeting" \ -Dextensions="kotlin,resteasy-jsonb" -cd kotlin-quickstart +cd rest-kotlin-quickstart ---- When adding `kotlin` to the extensions list, the Maven plugin will generate a project that is properly @@ -382,4 +385,9 @@ One thing to note is that the live reload feature is not available when making c == Packaging the application -As usual, the application can be packaged using `./mvnw clean package` and executed using the `-runner.jar` file. You can also build the native executable using `./mvnw package -Pnative`, or `./gradlew buildNative`. \ No newline at end of file +As usual, the application can be packaged using `./mvnw clean package` and executed using the `-runner.jar` file. You can also build the native executable using `./mvnw package -Pnative`, or `./gradlew buildNative`. + +== Kotlin and Jackson + +If the `com.fasterxml.jackson.module:jackson-module-kotlin` dependency and the `quarkus-jackson` extension (or the `quarkus-resteasy-extension`) have been added to project, +then Quarkus automatically registers the `KotlinModule` to the `ObjectMapper` bean (see link:rest-json#Jackson[this] guide for more details). \ No newline at end of file diff --git a/docs/src/main/asciidoc/kubernetes-client.adoc b/docs/src/main/asciidoc/kubernetes-client.adoc index 7e74ce209034f..6972a04acf030 100644 --- a/docs/src/main/asciidoc/kubernetes-client.adoc +++ b/docs/src/main/asciidoc/kubernetes-client.adoc @@ -22,7 +22,9 @@ Once you have your Quarkus project configured you can add the `kubernetes-client to your project by running the following command in your project base directory. [source] +---- ./mvnw quarkus:add-extension -Dextensions="kubernetes-client" +---- This will add the following to your pom.xml: diff --git a/docs/src/main/asciidoc/kubernetes.adoc b/docs/src/main/asciidoc/kubernetes.adoc index 8fb228c114447..d8d5953972b03 100644 --- a/docs/src/main/asciidoc/kubernetes.adoc +++ b/docs/src/main/asciidoc/kubernetes.adoc @@ -63,7 +63,7 @@ quarkus.application.name=test-quarkus-app # this is also optional and defaults t ---- and following the execution of `./mvnw package` you will notice amongst the other files that are created, two files named -`kubernetes.json` and `kubernetes.yaml` in the `target/kubernetes/` directory. +`kubernetes.json` and `kubernetes.yml` in the `target/kubernetes/` directory. If you look at either file you will see that it contains both a Kubernetes `Deployment` and a `Service`. @@ -178,7 +178,7 @@ The docker registry and the user of the docker image can be specified, with the [source] ---- kubernetes.group=myUser -kubernetes.registry=http://my.docker-registry.net +docker.registry=http://my.docker-registry.net ---- Note: These options used to be `quarkus.kubernetes.docker.registry` and `quarkus.kubernetes.group` respectively. @@ -209,39 +209,38 @@ The table below describe all the available configuration options. .Kubernetes |==== -| Property | Type | Description | Default Value -| kubernetes.group | String | | -| kubernetes.name | String | | -| kubernetes.version | String | | -| kubernetes.init-containers | Container[] | | -| kubernetes.labels | Label[] | | -| kubernetes.annotations | Annotation[] | | -| kubernetes.env-vars | Env[] | | -| kubernetes.working-dir | String | | -| kubernetes.command | String[] | | -| kubernetes.arguments | String[] | | -| kubernetes.replicas | int | | 1 -| kubernetes.service-account | String | | -| kubernetes.host | String | | -| kubernetes.ports | Port[] | | -| kubernetes.service-type | ServiceType | | ClusterIP -| kubernetes.pvc-volumes | PersistentVolumeClaimVolume[] | | -| kubernetes.secret-volumes | SecretVolume[] | | -| kubernetes.config-map-volumes | ConfigMapVolume[] | | -| kubernetes.git-repo-volumes | GitRepoVolume[] | | -| kubernetes.aws-elastic-block-store-volumes | AwsElasticBlockStoreVolume[] | | -| kubernetes.azure-disk-volumes | AzureDiskVolume[] | | -| kubernetes.azure-file-volumes | AzureFileVolume[] | | -| kubernetes.mounts | Mount[] | | -| kubernetes.image-pull-policy | ImagePullPolicy | | IfNotPresent -| kubernetes.image-pull-secrets | String[] | | -| kubernetes.liveness-probe | Probe | | -| kubernetes.readiness-probe | Probe | | -| kubernetes.sidecars | Container[] | | -| kubernetes.expose | boolean | | false -| kubernetes.docker-file | String | | Dockerfile -| kubernetes.registry | String | | -| kubernetes.auto-deploy-enabled | boolean | | false +| Property | Type | Description | Default Value +| kubernetes.group | String | | +| kubernetes.name | String | | +| kubernetes.version | String | | +| kubernetes.init-containers | Container[] | | +| kubernetes.labels | Label[] | | +| kubernetes.annotations | Annotation[] | | +| kubernetes.env-vars | Env[] | | +| kubernetes.working-dir | String | | +| kubernetes.command | String[] | | +| kubernetes.arguments | String[] | | +| kubernetes.replicas | int | | 1 +| kubernetes.service-account | String | | +| kubernetes.host | String | | +| kubernetes.ports | Port[] | | +| kubernetes.service-type | ServiceType | | ClusterIP +| kubernetes.pvc-volumes | PersistentVolumeClaimVolume[] | | +| kubernetes.secret-volumes | SecretVolume[] | | +| kubernetes.config-map-volumes | ConfigMapVolume[] | | +| kubernetes.git-repo-volumes | GitRepoVolume[] | | +| kubernetes.aws-elastic-block-store-volumes | AwsElasticBlockStoreVolume[] | | +| kubernetes.azure-disk-volumes | AzureDiskVolume[] | | +| kubernetes.azure-file-volumes | AzureFileVolume[] | | +| kubernetes.mounts | Mount[] | | +| kubernetes.image-pull-policy | ImagePullPolicy | | IfNotPresent +| kubernetes.image-pull-secrets | String[] | | +| kubernetes.liveness-probe | Probe | | ( see Probe ) +| kubernetes.readiness-probe | Probe | | ( see Probe ) +| kubernetes.sidecars | Container[] | | +| kubernetes.expose | boolean | | false +| kubernetes.headless | boolean | | false +| kubernetes.auto-deploy-enabled | boolean | | false |==== Properties that use non standard types, can be referenced by expanding the property. @@ -287,40 +286,39 @@ Below you will find tables describing all available types. .Probe |==== -| Property | Type | Description | Default Value -|---------------------+--------+-------------+--------------- -| http-action-path | String | | -| exec-action | String | | +| Property | Type | Description | Default Value +| http-action-path | String | | +| exec-action | String | | | tcp-socket-action | String | | | initial-delay-seconds | int | | 0 -| period-seconds | int | | 30 -| timeout-seconds | int | | 10 +| period-seconds | int | | 30 +| timeout-seconds | int | | 10 |==== .Port |==== -| Property | Type | Description | Default Value -| name | String | | -| container-port | int | | -| hostPort | int | | 0 -| path | String | | / -| protocol | Protocol | | TCP +| Property | Type | Description | Default Value +| name | String | | +| container-port | int | | +| host-port | int | | 0 +| path | String | | / +| protocol | Protocol | | TCP |==== .Container |==== -| Property | Type | Description | Default Value -| image | String | | -| name | String | | -| env-vars | Env[] | | -| working-dir | String | | -| command | String[] | | -| arguments | String[] | | -| ports | Port[] | | -| mounts | Mount[] | | +| Property | Type | Description | Default Value +| image | String | | +| name | String | | +| env-vars | Env[] | | +| working-dir | String | | +| command | String[] | | +| arguments | String[] | | +| ports | Port[] | | +| mounts | Mount[] | | | image-pull-policy | ImagePullPolicy | | IfNotPresent -| liveness-probe | Probe | | -| readiness-probe | Probe | | +| liveness-probe | Probe | | +| readiness-probe | Probe | | |==== @@ -328,39 +326,39 @@ Below you will find tables describing all available types. .Mount |==== -| Property | Type | Description | Default Value -| name | String | | -| path | String | | +| Property | Type | Description | Default Value +| name | String | | +| path | String | | | sub-path | String | | | read-only | boolean | | false |==== .ConfigMapVolume |==== -| Property | Type | Description | Default Value -| volume-name | String | | +| Property | Type | Description | Default Value +| volume-name | String | | | config-map-name | String | | -| default-mode | int | | 384 -| optional | boolean | | false +| default-mode | int | | 384 +| optional | boolean | | false |==== .SecretVolume |==== -| Property | Type | Description | Default Value +| Property | Type | Description | Default Value | volume-name | String | | | secret-name | String | | | default-mode | int | | 384 -| optional | boolean | | false +| optional | boolean | | false |==== .AzureDiskVolume |==== -| Property | Type | Description | Default Value +| Property | Type | Description | Default Value | volume-name | String | | | disk-name | String | | | disk-uri | String | | -| kind | String | | Managed +| kind | String | | Managed | caching-mode | String | | ReadWrite | fs-type | String | | ext4 | read-only | boolean | | false @@ -368,33 +366,33 @@ Below you will find tables describing all available types. .AwsElasticBlockStoreVolume |==== -| Property | Type | Description | Default Value +| Property | Type | Description | Default Value | volume-name | String | | | volume-id | String | | -| partition | int | | +| partition | int | | | fs-type | String | | ext4 | read-only | boolean | | false |==== .GitRepoVolume |==== -| Property | Type | Description | Default Value +| Property | Type | Description | Default Value | volume-name | String | | -| repository | String | | -| directory | String | | -| revision | String | | +| repository | String | | +| directory | String | | +| revision | String | | |==== .PersistentVolumeClaimVolume |==== -| Property | Type | Description | Default Value +| Property | Type | Description | Default Value | volume-name | String | | | claim-name | String | | | read-only | boolean | | false |==== .AzureFileVolume |==== -| Property | Type | Description | Default Value +| Property | Type | Description | Default Value | volume-name | String | | | share-name | String | | | secret-name | String | | @@ -405,11 +403,11 @@ Below you will find tables describing all available types. .Docker |==== -| Property | Type | Description | Default Value -| docker-file | String | | Dockerfile -| registry | String | | | -| auto-push-enabled | boolean | | false -| auto-build-enabled | boolean | | false +| Property | Type | Description | Default Value +| docker.docker-file | String | | Dockerfile +| docker.registry | String | | +| docker.auto-push-enabled | boolean | | false +| docker.auto-build-enabled | boolean | | false |==== === OpenShift support @@ -432,48 +430,51 @@ The OpenShift resources can be customized in a similar approach with Kubernetes. .Openshift |==== -| Property | Type | Description | Default Value -| openshift.group | String | | -| openshift.name | String | | -| openshift.version | String | | -| openshift.init-containers | Container[] | | -| openshift.labels | Label[] | | -| openshift.annotations | Annotation[] | | -| openshift.env-vars | Env[] | | -| openshift.working-dir | String | | -| openshift.command | String[] | | -| openshift.arguments | String[] | | -| openshift.replicas | int | | 1 -| openshift.service-account | String | | -| openshift.host | String | | -| openshift.ports | Port[] | | -| openshift.service-type | ServiceType | | ClusterIP -| openshift.pvc-volumes | PersistentVolumeClaimVolume[] | | -| openshift.secret-volumes | SecretVolume[] | | -| openshift.config-map-volumes | ConfigMapVolume[] | | -| openshift.git-repo-volumes | GitRepoVolume[] | | -| openshift.aws-elastic-block-store-volumes | AwsElasticBlockStoreVolume[] | | -| openshift.azure-disk-volumes | AzureDiskVolume[] | | -| openshift.azure-file-volumes | AzureFileVolume[] | | -| openshift.mounts | Mount[] | | -| openshift.image-pull-policy | ImagePullPolicy | | IfNotPresent -| openshift.image-pull-secrets | String[] | | -| openshift.liveness-probe | Probe | | ( see Probe ) -| openshift.readiness-probe | Probe | | ( see Probe ) -| openshift.sidecars | Container[] | | -| openshift.expose | boolean | | false -| openshift.auto-deploy-enabled | boolean | | false +| Property | Type | Description | Default Value +| openshift.group | String | | +| openshift.name | String | | +| openshift.version | String | | +| openshift.init-containers | Container[] | | +| openshift.labels | Label[] | | +| openshift.annotations | Annotation[] | | +| openshift.env-vars | Env[] | | +| openshift.working-dir | String | | +| openshift.command | String[] | | +| openshift.arguments | String[] | | +| openshift.replicas | int | | 1 +| openshift.service-account | String | | +| openshift.host | String | | +| openshift.ports | Port[] | | +| openshift.service-type | ServiceType | | ClusterIP +| openshift.pvc-volumes | PersistentVolumeClaimVolume[] | | +| openshift.secret-volumes | SecretVolume[] | | +| openshift.config-map-volumes | ConfigMapVolume[] | | +| openshift.git-repo-volumes | GitRepoVolume[] | | +| openshift.aws-elastic-block-store-volumes | AwsElasticBlockStoreVolume[] | | +| openshift.azure-disk-volumes | AzureDiskVolume[] | | +| openshift.azure-file-volumes | AzureFileVolume[] | | +| openshift.mounts | Mount[] | | +| openshift.image-pull-policy | ImagePullPolicy | | IfNotPresent +| openshift.image-pull-secrets | String[] | | +| openshift.liveness-probe | Probe | | ( see Probe ) +| openshift.readiness-probe | Probe | | ( see Probe ) +| openshift.sidecars | Container[] | | +| openshift.expose | boolean | | false +| openshift.headless | boolean | | false +| openshift.auto-deploy-enabled | boolean | | false |==== .S2i |==== -| Property | Type | Description | Default Value -| s2i.enabled | boolean | | true -| s2i.registry | String | | -| s2i.builder-image | String | | fabric8/s2i-java:2.3 -| s2i.build-env-vars | Env[] | | -| s2i.auto-push-enabled | boolean | | false -| s2i.auto-build-enabled | boolean | | false +| Property | Type | Description | Default Value +| s2i.enabled | boolean | | true +| s2i.docker-file | String | | Dockerfile +| s2i.registry | String | | +| s2i.builder-image | String | | fabric8/s2i-java:2.3 +| s2i.build-env-vars | Env[] | | +| s2i.auto-push-enabled | boolean | | false +| s2i.auto-build-enabled | boolean | | false +| s2i.auto-deploy-enabled | boolean | | false |==== === Knative @@ -530,33 +531,32 @@ The generated service can be customized using the following properties: .Knative |==== -| Property | Type | Description | Default Value -| knative.group | String | | -| knative.name | String | | -| knative.version | String | | -| knative.labels | Label[] | | -| knative.annotations | Annotation[] | | -| knative.env-vars | Env[] | | -| knative.working-dir | String | | -| knative.command | String[] | | -| knative.arguments | String[] | | -| knative.service-account | String | | -| knative.host | String | | -| knative.ports | Port[] | | -| knative.service-type | ServiceType | | ClusterIP -| knative.pvc-volumes | PersistentVolumeClaimVolume[] | | -| knative.secret-volumes | SecretVolume[] | | -| knative.config-map-volumes | ConfigMapVolume[] | | -| knative.git-repo-volumes | GitRepoVolume[] | | -| knative.aws-elastic-block-store-volumes | AwsElasticBlockStoreVolume[] | | -| knative.azure-disk-volumes | AzureDiskVolume[] | | -| knative.azure-file-volumes | AzureFileVolume[] | | -| knative.mounts | Mount[] | | -| knative.image-pull-policy | ImagePullPolicy | | IfNotPresent -| knative.image-pull-secrets | String[] | | -| knative.liveness-probe | Probe | | ( see Probe ) -| knative.readiness-probe | Probe | | ( see Probe ) -| knative.sidecars | Container[] | | -| knative.expose | boolean | | false -| knative.auto-deploy-enabled | boolean | | false -|==== \ No newline at end of file +| Property | Type | Description | Default Value +| knative.group | String | | +| knative.name | String | | +| knative.version | String | | +| knative.labels | Label[] | | +| knative.annotations | Annotation[] | | +| knative.env-vars | Env[] | | +| knative.working-dir | String | | +| knative.command | String[] | | +| knative.arguments | String[] | | +| knative.service-account | String | | +| knative.host | String | | +| knative.ports | Port[] | | +| knative.service-type | ServiceType | | ClusterIP +| knative.pvc-volumes | PersistentVolumeClaimVolume[] | | +| knative.secret-volumes | SecretVolume[] | | +| knative.config-map-volumes | ConfigMapVolume[] | | +| knative.git-repo-volumes | GitRepoVolume[] | | +| knative.aws-elastic-block-store-volumes | AwsElasticBlockStoreVolume[] | | +| knative.azure-disk-volumes | AzureDiskVolume[] | | +| knative.azure-file-volumes | AzureFileVolume[] | | +| knative.mounts | Mount[] | | +| knative.image-pull-policy | ImagePullPolicy | | IfNotPresent +| knative.image-pull-secrets | String[] | | +| knative.liveness-probe | Probe | | ( see Probe ) +| knative.readiness-probe | Probe | | ( see Probe ) +| knative.sidecars | Container[] | | +| knative.expose | boolean | | false +|==== diff --git a/docs/src/main/asciidoc/logging-sentry.adoc b/docs/src/main/asciidoc/logging-sentry.adoc new file mode 100644 index 0000000000000..d3f42560e2a2a --- /dev/null +++ b/docs/src/main/asciidoc/logging-sentry.adoc @@ -0,0 +1,85 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Logging to Sentry + +include::./attributes.adoc[] + +This guide explains how to configure Quarkus to log to Sentry. + +== Description + +Sentry is a really easy way to be notified of errors happening in your Quarkus application. + +It is a self-hosted and cloud-based error monitoring that helps software teams discover, triage, and prioritize errors in real-time. + +They offer a free starter price for cloud-based or you can self host it for free. + +WARNING: Sentry's Java SDK is open source, but recently sentry.io https://blog.sentry.io/2019/11/06/relicensing-sentry[changed the license] for their backend to the non-open source https://github.com/getsentry/sentry/blob/master/LICENSE[BSL license]. This might or might not be an issue for your project and product. + +== Configuration + +To start with, you need to get a Sentry DSN either by https://sentry.io/signup/[creating a Sentry account] or https://docs.sentry.io/server/[installing your own self-hosted Sentry]. + +In order to configure Sentry logging, add the `quarkus-logging-sentry` extension to your +application `pom.xml` as illustrated in the following snippet: + +.Add the Sentry logging extension to your pom.xml +[source,xml] +---- + + + + + io.quarkus + quarkus-logging-sentry + + + +---- + +[id="in-app-packages"] +=== “In Application” Stack Frames +Sentry differentiates stack frames that are directly related to your application (“in application”) from stack frames that come from other packages such as the standard library, frameworks, or other dependencies. The difference is visible in the Sentry web interface where only the “in application” frames are displayed by default. + +You can configure which package prefixes your application uses with the `in-app-packages` option, which takes a comma separated list of packages: + +[source, properties] +---- +quarkus.log.sentry.in-app-packages=com.mycompany,com.other.name +---- + +If you don’t want to use this feature but want to disable the warning, simply set it to `*`: + +[source, properties] +---- +quarkus.log.sentry.in-app-packages=* +---- + +== Example + +.All errors and warnings occurring in any of the packages will be sent to Sentry with DSN `https://abcd@sentry.io/1234` +[source, properties] +---- +quarkus.log.sentry=true +quarkus.log.sentry.dsn=https://abcd@sentry.io/1234 +quarkus.log.sentry.in-app-packages=* +---- + +.All errors occurring in the package `org.example` will be sent to Sentry with DSN `https://abcd@sentry.io/1234` +[source, properties] +---- +quarkus.log.sentry=true +quarkus.log.sentry.dsn=https://abcd@sentry.io/1234 +quarkus.log.sentry.level=ERROR +quarkus.log.sentry.in-app-packages=org.example +---- + +== Configuration Reference + +This extension is configured through the standard `application.properties` file. + +include::{generated-dir}/config/quarkus-logging-sentry.adoc[opts=optional, leveloffset=+1] + diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index 9006203979a27..d22eb94267da7 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -13,18 +13,22 @@ This guide explains logging and how to configure it. Run time configuration of logging is done through the normal `application.properties` file. -include::{generated-dir}/config/quarkus-core-log-config.adoc[opts=optional, leveloffset=+1] +include::{generated-dir}/config/quarkus-log-logging-log-config.adoc[opts=optional, leveloffset=+1] === Logging categories Logging is done on a per-category basis. Each category can be independently configured. A configuration which applies to a category will also apply to all sub-categories of that category, unless there is a more specific matching sub-category configuration. +For every category the same settings that are configured on ( console / file / syslog ) apply. +These can also be overridden by attaching a one or more named handlers to a category. See example in <> [cols="".level|INFO footnote:[Some extensions may define customized default log levels for certain categories, in order to reduce log noise by default. Setting the log level in configuration will override any extension-defined log levels.]|The level to use to configure the category named ``. The quotes are necessary. +|quarkus.log.category."".useParentHandlers|true|Specify whether or not this logger should send its output to its parent logger. +|quarkus.log.category."".handlers=[]|empty footnote:[By default the configured category gets the same handlers attached as the one on the root logger.]|The names of the handlers that you want to attach to a specific category. |=== NOTE: The quotes shown in the property name are required as categories normally contain '.' which must @@ -40,7 +44,7 @@ The root logger category is handled separately, and is configured via the follow |quarkus.log.level|INFO|The default minimum log level for every log category. |=== -[id="format_string"] +[id="format-string"] == Format String The logging format string supports the following symbols: @@ -75,6 +79,54 @@ The logging format string supports the following symbols: |%x|Nested Diagnostics context values|Renders all the values from Nested Diagnostics Context in format {value1.value2} |=== +[id="alt-console-format"] +=== Alternative Console Logging Formats + +It is possible to change the output format of the console log. This can be useful in environments where the output +of the Quarkus application is captured by a service which can, for example, process and store the log information for +later analysis. + +[id="json-logging"] +==== JSON Logging + +In order to configure JSON logging, the `quarkus-logging-json` extension may be employed. Add this extension to your +application POM as the following snippet illustrates. + +.Modifications to POM file to add the JSON logging extension +[source,xml] +---- + + + + io.quarkus + quarkus-logging-json + + +---- + +The presence of this extension will, by default, replace the output format configuration from the console configuration. +This means that the format string and the color settings (if any) will be ignored. The other console configuration items +(including those controlling asynchronous logging and the log level) will continue to be applied. + +For some, it will make sense to use logging that is humanly readable (unstructured) in dev mode and JSON logging (structured) in production mode. This can be achieved using different profiles, as shown in the following configuration. + +.Disable JSON logging in application.properties for dev and test mode +[source, properties] +---- +%dev.quarkus.log.console.json=false +%test.quarkus.log.console.json=false +---- + +===== Configuration + +The JSON logging extension can be configured in various ways. The following properties are supported: + +include::{generated-dir}/config/quarkus-logging-json.adoc[opts=optional, leveloffset=+1] + +WARNING: Enabling pretty printing might cause certain processors and JSON parsers to fail. + +NOTE: Printing the details can be expensive as the values are retrieved from the caller. The details include the +source class name, source file name, source method name and source line number. == Examples @@ -106,6 +158,23 @@ quarkus.log.category."io.quarkus.smallrye.jwt".level=TRACE quarkus.log.category."io.undertow.request.security".level=TRACE ---- +[#category-named-handlers-example] +.Named handlers attached to a category +[source, properties] +---- +# Send output to the console +quarkus.log.file.path=/tmp/trace.log +quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n +# Configure a named handler that logs to console +quarkus.log.handler.console."STRUCTURED_LOGGING".format=%e%n +# Configure a named handler that logs to file +quarkus.log.handler.file."STRUCTURED_LOGGING_FILE".enable=true +quarkus.log.handler.file."STRUCTURED_LOGGING_FILE".format=%e%n +# Configure the category and link the two named handlers to it +quarkus.log.category."io.quarkus.category".level=INFO +quarkus.log.category."io.quarkus.category".handlers=STRUCTURED_LOGGING,STRUCTURED_LOGGING_FILE +---- + == Supported Logging APIs Applications and components may use any of the following APIs for logging, and the logs will be merged: @@ -115,3 +184,6 @@ Applications and components may use any of the following APIs for logging, and t * https://www.slf4j.org/[SLF4J] * https://commons.apache.org/proper/commons-logging/[Apache Commons Logging] +== Centralized log management + +If you want to send your logs to a centralized tool like Graylog, Logstash or Fluentd, you can follow the link:centralized-log-management[Centralized log management guide]. diff --git a/docs/src/main/asciidoc/mailer.adoc b/docs/src/main/asciidoc/mailer.adoc index b3faf47c9eb31..4d3eea58fb0fa 100644 --- a/docs/src/main/asciidoc/mailer.adoc +++ b/docs/src/main/asciidoc/mailer.adoc @@ -60,8 +60,27 @@ quarkus.mailer.port=465 quarkus.mailer.ssl=true quarkus.mailer.username=.... quarkus.mailer.password=.... +quarkus.mailer.mock=false ---- +[NOTE] +==== +It is recommended to encrypt any sensitive data, such as the `quarkus.mailer.password`. +One approach is to save the value into a secure store like HashiCorp Vault, and refer to it from the configuration. +Assuming for instance that Vault contains key `mail-password` at path `myapps/myapp/myconfig`, then the mailer +extension can be simply configured as: +``` +... +# path within the kv secret engine where is located the application sensitive configuration +quarkus.vault.secret-config-kv-path=myapps/myapp/myconfig + +... +quarkus.mailer.password=${mail-password} +``` +Please note that the password value is evaluated only once, at startup time. If `mail-password` was changed in Vault, +the only way to get the new value would be to restart the application. +==== + [TIP] For more information about the Mailer extension configuration please refer to the <>. @@ -179,6 +198,33 @@ By spec, when you create the inline attachment, the content-id must be structure If you don't wrap your content-id between `<>`, it is automatically wrapped for you. When you want to reference your attachment, for instance in the `src` attribute, use `cid:id@domain` (without the `<` and `>`). +== Message Body Based on Qute Templates + +It's also possible to inject a mail template, where the message body is created automatically using link:qute[Qute templates]. + +[source, java] +---- +@Inject +MailTemplate hello; <1> + +@GET +@Path("/mail") +public CompletionStage send() { + return hello.to("to@acme.org") <2> + .subject("Hello from Qute template") + // the template looks like: Hello {name}! + .data("name", "John") <3> + .send() <4> + .thenApply(x -> Response.accepted().build()); +} +---- +<1> If there is no `@ResourcePath` qualifier provided, the field name is used to locate the template. In this particular case, we will use the `hello.html` and `hello.txt` templates to create the message body. +<2> Create a mail template instance and set the recipient. +<3> Set the data used in the template. +<4> `MailTemplate.send()` triggers the rendering and, once finished, sends the e-mail via a `Mailer` instance. + +TIP: Injected mail templates are validated during build. If there is no matching template in `src/main/resources/templates` the build fails. + == Testing email sending Because it is very inconvenient to send emails during development and testing, you can set the `quarkus.mailer.mock` boolean @@ -265,7 +311,7 @@ By default both the mailer and Gmail default to `XOAUTH2` which requires registe == Using SSL with native executables Note that if you enable SSL for the mailer and you want to build a native executable, you will need to enable the SSL support. -Please refer to the native-and-ssl[Using SSL With Native Executables] guide for more information. +Please refer to the link:native-and-ssl[Using SSL With Native Executables] guide for more information. == Using the underlying Vert.x Mail Client diff --git a/docs/src/main/asciidoc/maven-tooling.adoc b/docs/src/main/asciidoc/maven-tooling.adoc index 24f94e68ef531..fd79a46fda63a 100644 --- a/docs/src/main/asciidoc/maven-tooling.adoc +++ b/docs/src/main/asciidoc/maven-tooling.adoc @@ -44,6 +44,18 @@ The following table lists the attributes you can pass to the `create` command: | `1.0-SNAPSHOT` | The version of the created project +| `platformGroupId` +| `io.quarkus` +| The group id of the target platform. Given that all the existing platforms are coming from `io.quarkus` this one won't practically be used explicitly. But it's still an option. + +| `platformArtifactId` +| `quarkus-universe-bom` +| The artifact id of the target platform BOM. It should be `quarkus-bom` in order to use the locally built Quarkus. + +| `platformVersion` +| If it's not specified, the latest one will be resolved. +| The version of the platform you want the project to use. It can also accept a version range, in which case the latest from the specified range will be used. + | `className` | _Not created if omitted_ | The fully qualified name of the generated resource @@ -58,6 +70,8 @@ The following table lists the attributes you can pass to the `create` command: |=== +By default, the command will target the latest version of `quarkus-universe-bom` (unless specific coordinates have been specified). If you run offline however, it will look for the latest locally available and if `quarkus-universe-bom` (satisfying the default version range which is currently up to 2.0) is not available locally, it will fallback to the bundled platform based on `quarkus-bom` (the version will match the version of the plugin). + If you decide to generate a REST resource (using the `className` attribute), the endpoint is exposed at: `http://localhost:8080/$path`. If you use the default `path`, the URL is: http://localhost:8080/hello. @@ -252,9 +266,9 @@ In IntelliJ: In a separated terminal or in the embedded terminal, run `./mvnw compile quarkus:dev`. Enjoy! -**Apache Netbeans** +**Apache NetBeans** -In Netbeans: +In NetBeans: 1. Select `File -> Open Project` 2. Select the project root @@ -453,7 +467,7 @@ These are provided in `application.properties` the same as any other config prop The properties are shown below: -include::{generated-dir}/config/quarkus-core-package-config.adoc[opts=optional] +include::{generated-dir}/config/quarkus-package-pkg-package-config.adoc[opts=optional] [[custom-test-configuration-profile]] === Custom test configuration profile in JVM mode diff --git a/docs/src/main/asciidoc/microprofile-fault-tolerance.adoc b/docs/src/main/asciidoc/microprofile-fault-tolerance.adoc index 3b7dd7d40dd75..4a3f9427aaa42 100644 --- a/docs/src/main/asciidoc/microprofile-fault-tolerance.adoc +++ b/docs/src/main/asciidoc/microprofile-fault-tolerance.adoc @@ -297,7 +297,7 @@ Requests that do not time out should show two recommended coffee samples in JSON Let's make our recommendations feature even better by providing a fallback (and presumably faster) way of getting related coffees. -Add a fallback method to `CaffeeResource` and a `@Fallback` annotation to `CoffeeResource#recommendations()` method +Add a fallback method to `CoffeeResource` and a `@Fallback` annotation to `CoffeeResource#recommendations()` method as follows: [source,java] @@ -330,9 +330,9 @@ Check the server output to see that fallback is really happening: [source] ---- -2019-03-06 13:21:54,170 INFO [org.acm.fau.CoffeeResource] (pool-15-thread-3) CoffeeResource#recommendations() invocation #2 returning successfully -2019-03-06 13:21:55,159 ERROR [org.acm.fau.CoffeeResource] (pool-15-thread-4) CoffeeResource#recommendations() invocation #3 timed out after 248 ms -2019-03-06 13:21:55,161 INFO [org.acm.fau.CoffeeResource] (HystrixTimer-1) Falling back to RecommendationResource#fallbackRecommendations() +2020-01-09 13:21:34,250 INFO [org.acm.fau.CoffeeResource] (executor-thread-1) CoffeeResource#recommendations() invocation #1 returning successfully +2020-01-09 13:21:36,354 ERROR [org.acm.fau.CoffeeResource] (executor-thread-1) CoffeeResource#recommendations() invocation #2 timed out after 250 ms +2020-01-09 13:21:36,355 INFO [org.acm.fau.CoffeeResource] (executor-thread-1) Falling back to RecommendationResource#fallbackRecommendations() ---- NOTE: The fallback method is required to have the same parameters as the original method. diff --git a/docs/src/main/asciidoc/microprofile-metrics.adoc b/docs/src/main/asciidoc/microprofile-metrics.adoc index 92da35eac68b1..6ee9a90adc91f 100644 --- a/docs/src/main/asciidoc/microprofile-metrics.adoc +++ b/docs/src/main/asciidoc/microprofile-metrics.adoc @@ -17,6 +17,10 @@ The metrics can be read remotely using JSON format or the OpenMetrics format, so they can be processed by additional tools such as Prometheus, and stored for analysis and visualisation. +Apart from application-specific metrics, which are described in this guide, you may also utilize built-in metrics +exposed by various Quarkus extensions. These are described in the guide for each particular extension that supports +built-in metrics. + == Prerequisites To complete this guide, you need: diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc index 897a642f486b0..26853836ae261 100644 --- a/docs/src/main/asciidoc/mongodb-panache.adoc +++ b/docs/src/main/asciidoc/mongodb-panache.adoc @@ -7,6 +7,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc include::./attributes.adoc[] :config-file: application.properties +:extension-status: preview MongoDB is a well known NoSQL Database that is widely used, but using its raw API can be cumbersome as you need to express your entities and your queries as a MongoDB link:https://mongodb.github.io/mongo-java-driver/3.11/bson/documents/#document[`Document`]. @@ -14,7 +15,7 @@ MongoDB with Panache provides active record style entities (and repositories) li It is built on top of the link:mongodb[MongoDB Client] extension. -WARNING: This extension is still experimental, feedback is welcome on the https://groups.google.com/d/forum/quarkus-dev[mailing list] or as issues in our https://github.com/quarkusio/quarkus/issues[GitHub issue tracker]. +include::./status-include.adoc[] == First: an example @@ -191,6 +192,10 @@ List allPersons = Person.listAll(); // finding a specific person by ID person = Person.findById(personId); +// finding a specific person by ID via an Optional +Optional optional = Person.findByIdOptional(personId); +person = optional.orElseThrow(() -> new NotFoundException()); + // finding all living persons List livingPersons = Person.list("status", Status.Alive); @@ -307,7 +312,7 @@ MongoDB with Panache will then map it to a MongoDB native query. If your query does not start with `{`, we will consider it a PanacheQL query: -- `` (and single parameter) which will expand to `{'singleColumnName': '?'}` +- `` (and single parameter) which will expand to `{'singleColumnName': '?'}` - `` will expand to `{}` where we will map the PanacheQL query to MongoDB native query form. We support the following operators that will be mapped to the corresponding MongoDB operators: 'and', 'or' ( mixing 'and' and 'or' is not currently supported), '=', '>', '>=', '<', '<=', '!=', 'is null', 'is not null', and 'like' that is mapped to the MongoDB `$regex` operator. Here are some query examples: @@ -382,6 +387,54 @@ public class Person extends PanacheMongoEntity { Both `findByNameWithPanacheQLQuery()` and `findByNameWithNativeQuery()` methods will return the same result but query written in PanacheQL will use the entity field name: `name`, and native query will use the MongoDB field name: `lastname`. +== Query projection + +Query projection can be done with the `project(Class)` method on the `PanacheQuery` object that is returned by the `find()` methods. + +You can use it to restrict which fields will be returned by the database, +the ID field will always be returned but it's not mandatory to include it inside the projection class. + +For this, you need to create a class (a Pojo) that will only contain the projected fields. +This pojo needs to be annotated with `@ProjectionFor(Entity.class)` where `Entity` is the name of your entity class. +The field names, or getters, of the projection class will be used to restrict which properties will be loaded from the database. + +Projection can be done for both PanacheQL and native queries. + +[source,java] +---- +import io.quarkus.mongodb.panache.ProjectionFor; +import org.bson.codecs.pojo.annotations.BsonProperty; + +// using public fields +@ProjectionFor(Person.class) +public class PersonName { + public String name; +} + +// using getters +@ProjectionFor(Person.class) +public class PersonNameWithGetter { + private String name; + + public String getName(){ + return name; + } + + public void setName(String name){ + this.name = name; + } +} + +// only 'name' will be loaded from the database +PanacheQuery shortQuery = Person.find("status ", Status.Alive).project(PersonName.class); +PanacheQuery query = Person.find("'status': ?1", Status.Alive).project(PersonNameWithGetter.class); +PanacheQuery nativeQuery = Person.find("{'status': 'ALIVE'}", Status.Alive).project(PersonName.class); +---- + +TIP: Using @BsonProperty is not needed to define custom column mappings, as the mappings from the entity class will be used. + +TIP: You can have your projection class extends from another class. In this case, the parent class also needs to have use `@ProjectionFor` annotation. + == The DAO/Repository option Repository is a very popular pattern and can be very accurate for some use case, depending on @@ -443,7 +496,7 @@ In MongoDB with Panache the ID are defined by a field named `id` of the `org.bso but if you want ot customize them, once again we have you covered. You can specify your own ID strategy by extending `PanacheMongoEntityBase` instead of `PanacheMongoEntity`. Then -you just declare whatever ID you want as a public field by annotating it by @BsonId: +you just declare whatever ID you want as a public field by annotating it by `@BsonId`: [source,java] ---- @@ -514,4 +567,3 @@ If you define your entities in the same project where you build your Quarkus app If the entities come from external projects or jars, you can make sure that your jar is treated like a Quarkus application library by indexing it via Jandex, see link:cdi-reference#how-to-generate-a-jandex-index[How to Generate a Jandex Index] in the CDI guide. This will allow Quarkus to index and enhance your entities as if they were inside the current project. - diff --git a/docs/src/main/asciidoc/mongodb.adoc b/docs/src/main/asciidoc/mongodb.adoc index 1e874d641b7f5..c260744a37547 100644 --- a/docs/src/main/asciidoc/mongodb.adoc +++ b/docs/src/main/asciidoc/mongodb.adoc @@ -5,11 +5,14 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Using the MongoDB Client include::./attributes.adoc[] +:extension-status: preview MongoDB is a well known NoSQL Database that is widely used. In this guide, we see how you can get your REST services to use the MongoDB database. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -328,7 +331,7 @@ More information in the codec documentation: https://mongodb.github.io/mongo-jav ---- package org.acme.rest.json.codec; -import com.mongodb.MongoClient; +import com.mongodb.MongoClientSettings; import org.acme.rest.json.Fruit; import org.bson.*; import org.bson.codecs.Codec; @@ -343,7 +346,7 @@ public class FruitCodec implements CollectibleCodec { private final Codec documentCodec; public FruitCodec() { - this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class); + this.documentCodec = MongoClientSettings.getDefaultCodecRegistry().get(Document.class); } @Override @@ -467,6 +470,30 @@ public class CodecFruitService { The link:mongodb-panache[MongoDB with Panache] extension facilitates the usage of MongoDB by providing active record style entities (and repositories) like you have in link:hibernate-orm-panache.html[Hibernate ORM with Panache] and focuses on making your entities trivial and fun to write in Quarkus. +== Connection Health Check + +If you are using the `quarkus-smallrye-health` extension, `quarkus-mongodb` will automatically add a readiness health check +to validate the connection to the cluster. + +So when you access the `/health/ready` endpoint of your application you will have information about the connection validation status. + +This behavior can be disabled by setting the `quarkus.mongodb.health.enabled` property to `false` in your `application.properties`. + +== The legacy client + +We don't include the legacy MongoDB client by default. It contains the now retired MongoDB Java API (DB, DBCollection,... ) +and the `com.mongodb.MongoClient` that is now superseded by `com.mongodb.client.MongoClient`. + +If you want to use the legacy API, you need to add the following dependency to your `pom.xml`: + +[source,xml] +---- + + org.mongodb + mongodb-driver-legacy + +---- + == Building a native executable You can use the MongoDB client in a native executable. diff --git a/docs/src/main/asciidoc/native-and-ssl.adoc b/docs/src/main/asciidoc/native-and-ssl.adoc index a379b60e310b1..3baa641d49ec8 100644 --- a/docs/src/main/asciidoc/native-and-ssl.adoc +++ b/docs/src/main/asciidoc/native-and-ssl.adoc @@ -125,7 +125,7 @@ And build again: If you check carefully the native executable build options, you can see that the SSL related options are gone: ``` -[INFO] [io.quarkus.creator.phase.nativeimage.NativeImagePhase] /opt/graalvm/bin/native-image -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dcom.sun.xml.internal.bind.v2.bytecode.ClassTailor.noOptimize=true -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -jar rest-client-1.0-SNAPSHOT-runner.jar -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 -H:+PrintAnalysisCallTree -H:EnableURLProtocols=http -H:-SpawnIsolates -H:-JNI --no-server -H:-UseServiceLoaderFeature -H:+StackTrace +[INFO] [io.quarkus.creator.phase.nativeimage.NativeImagePhase] /opt/graalvm/bin/native-image -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dcom.sun.xml.internal.bind.v2.bytecode.ClassTailor.noOptimize=true -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -jar rest-client-1.0-SNAPSHOT-runner.jar -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 -H:+PrintAnalysisCallTree -H:EnableURLProtocols=http -H:-SpawnIsolates -H:+JNI --no-server -H:-UseServiceLoaderFeature -H:+StackTrace ``` And we end up with: diff --git a/docs/src/main/asciidoc/neo4j.adoc b/docs/src/main/asciidoc/neo4j.adoc index 568fe7dd81e05..87169cdfbf1ae 100644 --- a/docs/src/main/asciidoc/neo4j.adoc +++ b/docs/src/main/asciidoc/neo4j.adoc @@ -4,12 +4,12 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc //// = Quarkus - Neo4j -:neo4j_version: 3.5.6 +:neo4j_version: 4.0.0 include::./attributes.adoc[] +:extension-status: preview -https://neo4j.com[Neo4j] is a graph database management system developed by Neo4j, Inc. -Neo4j is a native graph database focused not only on the data itself, but especially on the relations between data. +https://neo4j.com[Neo4j] is a graph database management system developed by Neo4j, Inc. Neo4j is a native graph database focused not only on the data itself, but especially on the relations between data. Neo4j stores data as a property graph, which consists of vertices or nodes as we call them, connected with edges or relationships. Both of them can have properties. @@ -34,9 +34,9 @@ The driver itself is released under the Apache 2.0 license, while Neo4j itself is available in a GPL3-licensed open-source "community edition", with online backup and high availability extensions licensed under a closed-source commercial license. -== Programming model +include::./status-include.adoc[] -NOTE: The Neo4j extension is based on an alpha version of the Neo4j driver. Some interactions with the driver might change in the future. +== Programming model The driver and thus the Quarkus extension support three different programming models: @@ -273,14 +273,14 @@ It uses transaction functions of the driver: public CompletionStage create(Fruit fruit) { AsyncSession session = driver.asyncSession(); return session - .writeTransactionAsync(tx -> - tx.runAsync("CREATE (f:Fruit {name: $name}) RETURN f", Values.parameters("name", fruit.name)) + .writeTransactionAsync(tx -> tx + .runAsync("CREATE (f:Fruit {name: $name}) RETURN f", Values.parameters("name", fruit.name)) + .thenCompose(fn -> fn.singleAsync()) ) - .thenCompose(fn -> fn.singleAsync()) .thenApply(record -> Fruit.from(record.get("f").asNode())) - .thenCompose(persistedFruid -> session.closeAsync().thenApply(signal -> persistedFruid)) - .thenApply(persistedFruid -> Response - .created(URI.create("/fruits/" + persistedFruid.id)) + .thenCompose(persistedFruit -> session.closeAsync().thenApply(signal -> persistedFruit)) + .thenApply(persistedFruit -> Response + .created(URI.create("/fruits/" + persistedFruit.id)) .build() ); } @@ -315,10 +315,10 @@ We also add some exception handling, in case the resource is called with an inva public CompletionStage getSingle(@PathParam("id") Long id) { AsyncSession session = driver.asyncSession(); return session - .readTransactionAsync(tx -> - tx.runAsync("MATCH (f:Fruit) WHERE id(f) = $id RETURN f", Values.parameters("id", id)) + .readTransactionAsync(tx -> tx + .runAsync("MATCH (f:Fruit) WHERE id(f) = $id RETURN f", Values.parameters("id", id)) + .thenCompose(fn -> fn.singleAsync()) ) - .thenCompose(fn -> fn.singleAsync()) .handle((record, exception) -> { if(exception != null) { Throwable source = exception; @@ -362,10 +362,10 @@ public CompletionStage delete(@PathParam("id") Long id) { AsyncSession session = driver.asyncSession(); return session - .writeTransactionAsync(tx -> - tx.runAsync("MATCH (f:Fruit) WHERE id(f) = $id DELETE f", Values.parameters("id", id)) + .writeTransactionAsync(tx -> tx + .runAsync("MATCH (f:Fruit) WHERE id(f) = $id DELETE f", Values.parameters("id", id)) + .thenCompose(fn -> fn.consumeAsync()) // <1> ) - .thenCompose(fn -> fn.consumeAsync()) // <1> .thenCompose(response -> session.closeAsync()) .thenApply(signal -> Response.noContent().build()); } @@ -394,6 +394,15 @@ It can be run with `java -jar target/neo4j-quickstart-1.0-SNAPSHOT-runner.jar`. With GraalVM installed, you can also create a native executable binary: `./mvnw clean package -Dnative`. Depending on your system, that will take some time. +=== Connection Health Check + +If you are using the `quarkus-smallrye-health` extension, `quarkus-neo4j` will automatically add a readiness health check +to validate the connection to Neo4j. + +So when you access the `/health/ready` endpoint of your application you will have information about the connection validation status. + +This behavior can be disabled by setting the `quarkus.neo4j.health.enabled` property to `false` in your `application.properties`. + === Explore Cypher and the Graph There are tons of options to model your domain within a Graph. @@ -414,7 +423,7 @@ Please add the following dependency management and dependency to your `pom.xml` io.projectreactor reactor-bom - Californium-SR4 + Dysprosium-RELEASE pom import @@ -445,7 +454,7 @@ import javax.ws.rs.core.MediaType; import org.neo4j.driver.Driver; import org.neo4j.driver.reactive.RxSession; -import org.neo4j.driver.reactive.RxStatementResult; +import org.neo4j.driver.reactive.RxResult; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -462,7 +471,7 @@ public class ReactiveFruitResource { @Produces(MediaType.SERVER_SENT_EVENTS) public Publisher get() { return Flux.using(driver::rxSession, session -> session.readTransaction(tx -> { - RxStatementResult result = tx.run("MATCH (f:Fruit) RETURN f.name as name ORDER BY f.name"); + RxResult result = tx.run("MATCH (f:Fruit) RETURN f.name as name ORDER BY f.name"); return Flux.from(result.records()).map(record -> record.get("name").asString()); }), RxSession::close); } diff --git a/docs/src/main/asciidoc/openapi-swaggerui.adoc b/docs/src/main/asciidoc/openapi-swaggerui.adoc index 9189b1e68d22b..73cd6a70d5c08 100644 --- a/docs/src/main/asciidoc/openapi-swaggerui.adoc +++ b/docs/src/main/asciidoc/openapi-swaggerui.adoc @@ -250,6 +250,44 @@ quarkus.smallrye-openapi.path=/swagger Hit `CTRL+C` to stop the application. +== Providing Application Level OpenAPI Annotations + +There are some MicroProfile OpenAPI annotations which describe global API information, such as the following: + +* API Title +* API Description +* Version +* Contact Information +* License + +All of this information (and more) can be included in your Java code by using appropriate OpenAPI annotations +on a JAX-RS `Application` class. Because a JAX-RS `Application` class is not required in Quarkus, you will +likely have to create one. It can simply be an empty class that extends `javax.ws.rs.core.Application`. This +empty class can then be annotated with various OpenAPI annotations such as `@OpenAPIDefinition`. For example: + +[source, java] +---- +@OpenAPIDefinition( + tags = { + @Tag(name="widget", description="Widget operations."), + @Tag(name="gasket", description="Operations related to gaskets") + }, + info = @Info( + title="Example API", + version = "1.0.1", + contact = @Contact( + name = "Example API Support", + url = "http://exampleurl.com/contact", + email = "techsupport@example.com"), + license = @License( + name = "Apache 2.0", + url = "http://www.apache.org/licenses/LICENSE-2.0.html")) +) +public class ExampleApiApplication extends Application { +} +---- + + == Loading OpenAPI Schema From Static Files Instead of dynamically creating OpenAPI schemas from annotation scanning, Quarkus also supports serving static OpenAPI documents. @@ -398,4 +436,3 @@ include::{generated-dir}/config/quarkus-smallrye-openapi.adoc[opts=optional, lev === Swagger UI include::{generated-dir}/config/quarkus-swaggerui.adoc[opts=optional, leveloffset=+1] - diff --git a/docs/src/main/asciidoc/quartz.adoc b/docs/src/main/asciidoc/quartz.adoc new file mode 100644 index 0000000000000..589d561dda8a0 --- /dev/null +++ b/docs/src/main/asciidoc/quartz.adoc @@ -0,0 +1,336 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Scheduling Periodic Tasks with Quartz + +include::./attributes.adoc[] +:extension-status: preview + +Modern applications often need to run specific tasks periodically. +In this guide, you learn how to schedule periodic clustered tasks using the http://www.quartz-scheduler.org/[Quartz] extension. + +include::./status-include.adoc[] + +TIP: If you only need to run in-memory scheduler use the link:scheduler[Scheduler] extension. + +== Prerequisites + +To complete this guide, you need: + +* less than 10 minutes +* an IDE +* JDK 1.8+ installed with `JAVA_HOME` configured appropriately +* Apache Maven 3.5.3+ +* Docker and Docker Compose installed on your machine + +== Architecture + +In this guide, we are going to expose one Rest API `tasks` to visualise the list of tasks created by a Quartz job running every 10 seconds. + +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `quartz-quickstart` {quickstarts-tree-url}/quartz-quickstart[directory]. + +== Creating the Maven project + +First, we need a new project. Create a new project with the following command: + +[source,shell,subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=quartz-quickstart \ + -DclassName="org.acme.quartz.TaskResource" \ + -Dpath="/tasks" \ + -Dextensions="quartz, hibernate-orm-panache, flyway, resteasy-jsonb, jdbc-postgresql" +cd quartz-quickstart +---- + +It generates: + +* the Maven structure +* a landing page accessible on `http://localhost:8080` +* example `Dockerfile` files for both `native` and `jvm` modes +* the application configuration file +* an `org.acme.quartz.TaskResource` resource +* an associated test + +The Maven project also imports the Quarkus Quartz extension. + +== Creating the Task Entity + +In the `org.acme.quartz` package, create the `Task` class, with the following content: + +[source,java] +---- +package org.acme.quartz; + +import javax.persistence.Entity; +import java.time.Instant; +import javax.persistence.Table; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; + +@Entity +@Table(name="TASKS") +public class Task extends PanacheEntity { <1> + public Instant createdAt; + + public Task() { + createdAt = Instant.now(); + } + + public Task(Instant time) { + this.createdAt = time; + } +} +---- +1. Declare the entity using https://quarkus.io/guides/hibernate-orm-panache[Panache] + +== Creating a scheduled job + +In the `org.acme.quartz` package, create the `TaskBean` class, with the following content: + +[source,java] +---- +package org.acme.quartz; + +import javax.enterprise.context.ApplicationScoped; + +import javax.transaction.Transactional; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped <1> +public class TaskBean { + + @Transactional + @Scheduled(every = "10s") <2> + void schedule() { + Task task = new Task(); <3> + task.persist(); <4> + } +} +---- +1. Declare the bean in the _application_ scope +2. Use the `@Scheduled` annotation to instruct Quarkus to run this method every 10 seconds. +3. Create a new `Task` with the current start time. +4. Persist the task in database using https://quarkus.io/guides/hibernate-orm-panache[Panache]. + +== Updating the application configuration file + +Edit the `application.properties` file and add the below configuration: +[source,shell] +---- +# Quartz configuration +quarkus.quartz.clustered=true <1> +quarkus.quartz.store-type=db <2> + +# Datasource configuration. +quarkus.datasource.url=jdbc:postgresql://localhost/quarkus_test +quarkus.datasource.driver=org.postgresql.Driver +quarkus.datasource.username=quarkus_test +quarkus.datasource.password=quarkus_test + +# Hibernate configuration +quarkus.hibernate-orm.database.generation=none +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.sql-load-script=no-file + +# flyway configuration +quarkus.flyway.connect-retries=10 +quarkus.flyway.table=flyway_quarkus_history +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0 +quarkus.flyway.baseline-description=Quartz +---- + +1. Indicate that the scheduler will be run in clustered mode +2. Use the database store to persist job related information so that they can be shared between nodes + +== Updating the resource and the test + +Edit the `TaskResource` class, and update the content to: + +[source,java] +---- +package org.acme.quartz; + +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/tasks") +@Produces(MediaType.APPLICATION_JSON) +public class TaskResource { + + @GET + public List listAll() { + return Task.listAll(); <1> + } +} +---- +1. Retrieve the list of created tasks from the database + +We also need to update the tests. Edit the `TaskResourceTest` class to match: + +[source,java] +---- +package org.acme.quartz; + +import io.quarkus.test.junit.QuarkusTest; + +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class TaskResourceTest { + + @Test + public void tasks() throws InterruptedException { + Thread.sleep(1000); // wait at least a second to have the first task created + given() + .when().get("/tasks") + .then() + .statusCode(200) + .body("size()", is(greaterThanOrEqualTo(1))); <1> + } +} +---- +1. Ensure that we have a `200` response and at least one task created + +== Creating Quartz Tables + +Add a SQL migration file named `src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql` with the content copied from +file with the content from {quickstarts-blob-url}/quartz-quickstart/src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql[V2.0.0__QuarkusQuartzTasks.sql]. + +== Configuring the load balancer + +In the root directory, create a `nginx.conf` file with the following content: + +[source,conf] +---- +user nginx; + +events { + worker_connections 1000; +} + +http { + server { + listen 8080; + location / { + proxy_pass http://tasks:8080; <1> + } + } +} +---- + +1. Route all traffic to our tasks application + +== Setting Application Deployment + +In the root directory, create a `docker-compose.yml` file with the following content: + +[source,yaml] +---- +version: '3' + +services: + tasks: <1> + image: quarkus-quickstarts/quartz:1.0 + build: + context: ./ + dockerfile: src/main/docker/Dockerfile.${QUARKUS_MODE:-jvm} + environment: + QUARKUS_DATASOURCE_URL: jdbc:postgresql://postgres/quarkus_test + networks: + - tasks-network + depends_on: + - postgres + + nginx: <2> + image: nginx:1.17.6 + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - tasks + ports: + - 8080:8080 + networks: + - tasks-network + + postgres: <3> + image: postgres:11.3 + container_name: quarkus_test + environment: + - POSTGRES_USER=quarkus_test + - POSTGRES_PASSWORD=quarkus_test + - POSTGRES_DB=quarkus_test + ports: + - 5432:5432 + networks: + - tasks-network + +networks: + tasks-network: + driver: bridge +---- + +1. Define the tasks service +2. Define the nginx load balancer to route incoming traffic to an appropriate node +3. Define the configuration to run the database + +== Running the database + +In a separate terminal, run the below command: + +[source,shell] +---- +docker-compose up postgres <1> +---- + +1. Start the database instance using the configuration options supplied in the `docker-compose.yml` file + +== Run the application in Dev Mode + +Run the application with: `./mvnw quarkus:dev`. +After a few seconds, open another terminal and run `curl localhost:8080/tasks` to verify that we have at least one task created. + +As usual, the application can be packaged using `./mvnw clean package` and executed using the `-runner.jar` file. +You can also generate the native executable with `./mvnw clean package -Pnative`. + +== Packaging the application and run several instances + +The application can be packaged using `./mvnw clean package`. Once the build is successful, run the below command: + +[source,shell] +---- +docker-compose up --scale tasks=2 --scale nginx=1 <1> +---- + +1. Start two instances of the application and a load balancer + +After a few seconds, in another terminal, run `curl localhost:8080/tasks` to verify that tasks were only created at different instants and in an interval of 10 seconds. + +You can also generate the native executable with `./mvnw clean package -Pnative`. + +[[quartz-configuration-reference]] +== Quartz Configuration Reference + +include::{generated-dir}/config/quarkus-quartz.adoc[leveloffset=+1, opts=optional] diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc new file mode 100644 index 0000000000000..efc3681c1e379 --- /dev/null +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -0,0 +1,795 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Qute Reference Guide + +include::./attributes.adoc[] + +:numbered: +:sectnums: +:sectnumlevels: 4 +:toc: +:extension-status: experimental + +include::./status-include.adoc[] + +== Hello World Example + +In this example, we'd like to demonstrate the basic workflow when working with Qute templates. +Let's start with a simple hello world example. +We will always need some *template contents*: + +.hello.html +[source,html] +---- + +

Hello {name}! <1> + +---- +<1> `{name}` is a value expression that is evaluated when the template is rendered. + +Then, we will need to parse the contents into a *template definition* Java object. +A template definition is an instance of `io.quarkus.qute.Template`. + +If using Qute "standalone" you'll need to create an instance of `io.quarkus.qute.Engine` first. +The `Engine` represents a central point for template management with dedicated configuration. +Let's use the convenient builder: + +[source,java] +---- +Engine engine = Engine.builder().addDefaults().build(); +---- + +TIP: In Quarkus, there is a preconfigured `Engine` available for injection - see <>. + +If we have an `Engine` instance we could parse the template contents: + +[source,java] +---- +Template helloTemplate = engine.parse(helloHtmlContent); +---- + +TIP: In Quarkus, you can simply inject the template definition. The template is automatically parsed and cached - see <>. + +Finally, we will create a *template instance*, set the data and render the output: + +[source,java] +---- +// Renders

Hello Jim!

+helloTemplate.data("name", "Jim").render(); <1> +---- +<1> `Template.data(String, Object)` is a convenient method that creates a template instance and sets the data in one step. + +So the workflow is simple: + +1. Create template contents (`hello.html`), +2. Parse template definition (`io.quarkus.qute.Template`), +3. Create template instance (`io.quarkus.qute.TemplateInstance`), +4. Render output. + +The `Engine` is able to cache the definitions so that it's not necessary to parse the contents again and again. + +NOTE: In Quarkus, the caching is done automatically. + +== Core Features + +=== Syntax and Building Blocks + +The dynamic parts of a template include: + +* *comment* +** `{! This is a comment !}`, +** could be multi-line, +** may contain expressions and sections: `{! {#if true} !}`. +* *expression* +** outputs the evaluated value, +** simple properties: `{foo}`, `{item.name}`, +** virtual methods: `{item.get(name)}`, `{name ?: 'John'}`, +** with namespace: `{inject:colors}`. +* *section tag* +** may contain expressions and sections: `{#if foo}{foo.name}{/if}`, +** the name in the closing tag is optional: `{#if active}ACTIVE!{/}`, +** can be empty: `{#myTag image=true /}`, +** may declare nested section blocks: `{#if item.valid} Valid. {#else} Invalid. {/if}` and decide which block to render. + +==== Expressions + +An expression consists of: + +* an optional namespace followed by a colon `:`, +* parts separated by dot `.`. + +The first part of the expression is always resolved against the <>. +If no result is found for the first part it's resolved against the parent context object (if available). +For an expression that starts with a namespace the current context object is found using the available ``NamespaceResolver``s. +For an expression that does not start with a namespace the current context object is *derived from the position* of the tag. +All other parts are resolved using `ValueResolver` s against the result of the previous resolution. + +For example, expression `{name}` has no namespace and single part - "name". +The "name" will be resolved using all available value resolvers against the current context object. +However, the expression `{global:colors}` has the namespace "global" and single part - "colors". +First, all available `NamespaceResolver` s will be used to find the current context object. +And afterwards value resolvers will be used to resolve "colors" against the context object found. + +[source] +---- +{name} <1> +{item.name} <2> +{global:colors} <3> +---- +<1> no namespace, one part -`name` +<2> no namespace, two parts - `item`, `name` +<3> namespace `global`, one part - `colors` + +An expression part could be a "virtual method" in which case the name can be followed by parameters in parentheses. + +[source] +---- +{item.getLabels(1)} <1> +{name or 'John'} <2> +---- +<1> no namespace, two parts - `item`, `getLabels(1)`, the second part is a virtual method with name `getLabels` and params `1` +<2> infix notation, translated to `name.or('John')`; no namespace, two parts - `name`, `or('John')` + +[[current_context_object]] +===== Current Context + +If an expression does not specify a namespace the current context object is derived from the position of the tag. +By default, the current context object represents the data passed to the template instance. +However, sections may change the current context object. +A typical example is the for/each loop - during iteration the content of the section is rendered with each element as the current context object: + +[source] +---- +{#each items} + {count}. {it.name} <1> +{/each} + +{! Another form of iteration... !} +{#for item in items} + {count}. {item.name} <2> +{/for} +---- +<1> `it` is an implicit alias. `name` is resolved against the current iteration element. +<2> Loop with an explicit alias `item`. + +[TIP] +==== +Data passed to the template instance are always accessible using the `data` namespace. +This could be useful to access data for which the key is overriden: + +[source,html] +---- + +{item.name} <1> +
    +{#for item in item.getDerivedItems()} <2> +
  • + {item.name} <3> + is derived from + {data:item.name} <4> +
  • +{/for} +
+ +---- +<1> `item` is passed to the template instance as a data object. +<2> Iterate over the list of derived items. +<3> `item` is an alias for the iterated element. +<4> Use the `data` namespace to access the `item` data object. + +==== + +===== Character Escapes + +For HTML and XML templates the `'`, `"`, `<`, `>`, `&` characters are escaped by default. +If you need to render the unescaped value: + +1. Use the `raw` or `safe` properties implemented as extension methods of the `java.lang.Object`, +2. Wrap the `String` value in a `io.quarkus.qute.RawString`. + +[source,html] +---- + +

{title}

<1> +{paragraph.raw} <2> + +---- +<1> `title` that resolves to `Expressions & Escapes` will be rendered as `Expressions &amp; Escapes` +<2> `paragraph` that resolves to `

My text!

` will be rendered as `

My text!

` + +==== Sections + +A section: + +* has a start tag +** starts with `#`, followed by the name of the section such as `{#if}` and `{#each}`, +* may be empty +** tag ends with `/`, ie. `{#emptySection /}` +* may contain other expression, sections, etc. +** the end tag starts with `/` and contains the name of the section (optional): `{#if foo}Foo!{/if}` or `{#if foo}Foo!{/}`, + +The start tag can also define parameters. +The parameters have optional names. +A section may contain several content *blocks*. +The "main" block is always present. +Additional/nested blocks also start with `#` and can have parameters too - `{#else if item.isActive}`. +A section helper that defines the logic of a section can "execute" any of the blocks and evaluate the parameters. + +[source] +---- +{#if item.name is 'sword'} + It's a sword! +{#else if item.name is 'shield'} + It's a shield! +{#else} + Item is neither a sword nor a shield. +{/if} +---- + +===== Loop Section + +The loop section makes it possible to iterate over an instance of `Iterable`, `Map` 's entry set and `Stream`. +It has two flavors. +The first one is using the `each` name alias. + +[source] +---- +{#each items} + {it.name} <1> +{/each} +---- +<1> `it` is an implicit alias. `name` is resolved against the current iteration element. + +The other form is using the `for` name alias and can specify the alias used to reference the iteration element: + +[source] +---- +{#for item in items} + {item.name} +{/for} +---- + +It's also possible to access the iteration metadata inside the loop: + +[source] +---- +{#each items} + {count}. {it.name} <1> +{/each} +---- +<1> `count` represents one-based index. Metadata also include zero-based `index`, `hasNext`, `odd`, `even`. + +===== If Section + +A basic control flow section. +The simplest possible version accepts a single parameter and renders the content if it's evaluated to `true` (or `Boolean.TRUE`). + +[source] +---- +{#if item.active} + This item is active. +{/if} +---- + +You can also use the following operators: + +|=== +|Operator |Aliases |Precedence (higher wins) + +|logical complement +|`!` +| 4 + +|greater than +|`gt`, `>` +| 3 + +|greater than or equal to +|`ge`, `>=` +| 3 + +|less than +|`lt`, `<` +| 3 + +|less than or equal to +|`le`, `\<=` +| 3 + +|equals +|`eq`, `==`, `is` +| 2 + +|not equals +|`ne`, `!=` +| 2 + +|logical AND (short-circuiting) +|`&&`, `and` +| 1 + +|logical OR (short-circuiting) +|`\|\|`, `or` +| 1 + +|=== + +.A simple operator example +[source] +---- +{#if item.age > 10} + This item is very old. +{/if} +---- + +Multiple conditions are also supported. + +.Multiple conditions example +[source] +---- +{#if item.age > 10 && item.price > 500} + This item is very old and expensive. +{/if} +---- + +Precedence rules can be overridden by parentheses. + +.Parentheses example +[source] +---- +{#if (item.age > 10 || item.price > 500) && user.loggedIn} + User must be logged in and item age must be > 10 or price must be > 500. +{/if} +---- + + +You can add any number of `else` blocks: + +[source] +---- +{#if item.age > 10} + This item is very old. +{#else if item.age > 5} + This item is quite old. +{#else if item.age > 2} + This item is old. +{#else} + This item is not old at all! +{/if} +---- + +===== With Section + +This section can be used to set the current context object. +This could be useful to simplify the template structure. + +[source] +---- +{#with item.parent} + {name} <1> +{/with} +---- +<1> The name will be resolved against the `item.parent`. + +It's also possible to specify an alias for the context object: + +[source] +---- +{#with item.parent as myParent} + {myParent.name} +{/with} +---- + +[[include_helper]] +===== Include/Insert Sections + +These sections can be used to include another template and possibly override some parts of the template (template inheritance). + +.Template "base" +[source,html] +---- + + + +{#insert title}Default Title{/} <1> + + + {#insert body}No body!{/} <2> + + +---- +<1> `insert` sections are used to specify parts that could be overriden by a template that includes the given template. +<2> An `insert` section may define the default content that is rendered if not overriden. + +.Template "detail" +[source,html] +---- +{#include base} <1> + {#title}My Title{/title} <2> + {#body} +
+ My body. +
+{/include} +---- +<1> `include` section is used to specify the extended template. +<2> Nested blocks are used to specify the parts that should be overriden. + +NOTE: Section blocks can also define an optional end tag - `{/title}`. + +[[user_tags]] +===== User-defined Tags + +User-defined tags can be used to include a template and optionally pass some parameters. +Let's suppose we have a template called `item.html`: + +[source] +---- +{#if showImage} <1> + {it.image} <2> +{/if} +---- +<1> `showImage` is a named parameter. +<2> `it` is a special key that is replaced with the first unnamed param of the tag. + +Now if we register this template under the name `item` and if we add a `UserTagSectionHelper` to the engine: + +[source,java] +---- +Engine engine = Engine.builder() + .addSectionHelper(new UserTagSectionHelper.Factory("item")) + .build(); +---- + +NOTE: In Quarkus, all files from the `src/main/resources/templates/tags` are registered and monitored automatically. + +We can include the tag like this: + +[source,html] +---- +
    +{#each items} +
  • + {#item this showImage=true /} <1> +
  • +{/each} +
+---- +<1> `this` is resolved to an iteration element and can be referenced using the `it` key in the tag template. + +=== Engine Configuration + +==== Template Locator + +Manual registration is sometimes handy but it's also possible to register a template locator using `EngineBuilder.addLocator(Function>)`. +This locator is used whenever the `Engine.getTemplate()` method is called and the engine has no template for a given id stored in the cache. + +NOTE: In Quarkus, all templates from the `src/main/resources/templates` are located automatically. + +[[quarkus_integration]] +== Quarkus Integration + +If you want to use Qute in your Quarkus application add the following dependency to your project: + +[source,xml] +---- + + io.quarkus + quarkus-qute + +---- + +In Quarkus, a preconfigured engine instance is provided and available for injection - a bean with scope `@Singleton`, bean type `io.quarkus.qute.Engine` and qualifier `@Default` is registered automatically. +Moreover, all templates located in the `src/main/resources/templates` directory are validated and can be easily injected. + +[source,java] +---- +import io.quarkus.qute.Engine; +import io.quarkus.qute.Template; +import io.quarkus.qute.api.ResourcePath; + +class MyBean { + + @Inject + Template items; <1> + + @ResourcePath("detail/items2_v1.html") <2> + Template items2; + + @Inject + Engine engine; <3> +} +---- +<1> If there is no `ResourcePath` qualifier provided, the field name is used to locate the template. In this particular case, the container will attempt to locate a template with path `src/main/resources/templates/items.html`. +<2> The `ResourcePath` qualifier instructs the container to inject a template from a path relative from `src/main/resources/templates`. In this case, the full path is `src/main/resources/templates/detail/items2_v1.html`. +<3> Inject the configured `Engine` instance. + +=== Injecting Beans Directly In Templates + +A CDI bean annotated with `@Named` can be referenced in any template through the `inject` namespace: + +[source,html] +---- +{inject:foo.price} <1> +---- +<1> First, a bean with name `foo` is found and then used as the base object. + +All expressions using the `inject` namespace are validated during build. +For the expression `inject:foo.price` the implementation class of the injected bean must either have the `price` property (e.g. a `getPrice()` method) or a matching <> must exist. + +NOTE: A `ValueResolver` is also generated for all beans annotated with `@Named` so that it's possible to access its properties without reflection. + +=== Parameter Declarations + +It is possible to specify optional parameter declarations in a template. +Quarkus attempts to validate all expressions that reference such parameters. +If an invalid/incorrect expression is found the build fails. + +NOTE: Only properties are currently validated in expressions; virtual methods are currently ignored. + +[source,html] +---- +{@org.acme.Foo foo} <1> + + + + +Qute Hello + + +

{title}

<2> + Hello {foo.message}! <3> + + +---- +<1> Parameter declaration - maps `foo` to `org.acme.Foo`. +<2> Not validated - not matching a param declaration. +<3> This expression is validated. `org.acme.Foo` must have a property `message` or a matching template extension method must exist. + +NOTE: A value resolver is also generated for all types used in parameter declarations so that it's possible to access its properties without reflection. + +==== Overriding Parameter Declarations + +[source,html] +---- +{@org.acme.Foo foo} + + + + +Qute Hello + + +

{foo.message}

<1> + {#for foo in baz.foos} +

Hello {foo.message}!

<2> + {/for} + + +---- +<1> Validated against `org.acme.Foo`. +<2> Not validated - `foo` is overridden in the loop section. + +[[template_extension_methods]] +=== Template Extension Methods + +Extension methods can be used to extend the data classes with new functionality. +For example, it is possible to add "computed properties" and "virtual methods". +A value resolver is automatically generated for a method annotated with `@TemplateExtension`. +If declared on a class a value resolver is generated for every non-private method declared on the class. +Methods that do not meet the following requirements are ignored. + +A template extension method: + +* must be static, +* must not return `void`, +* must accept at least one parameter. + +The class of the first parameter is always used to match the base object. +The method name is used to match the property name by default. +However, it is possible to specify the matching name with `TemplateExtension#matchName()`. + +NOTE: A special constant - `ANY` - may be used to specify that the extension method matches any name. In that case, the method must declare at least two parameters and the second parameter must be a string. + +.Extension Method Example +[source,java] +---- +package org.acme; + +class Item { + + public final BigDecimal price; + + public Item(BigDecimal price) { + this.price = price; + } +} + +@TemplateExtension +class MyExtensions { + + static BigDecimal discountedPrice(Item item) { <1> + return item.getPrice().multiply(new BigDecimal("0.9")); + } +} +---- +<1> This method matches an expression with base object of the type `Item.class` and the `discountedPrice` property name. + +This template extension method makes it possible to render the following template: + +[source,html] +---- +{item.discountedPrice} <1> +---- +<1> `item` is resolved to an instance of `org.acme.Item`. + +==== Method Parameters + +An extension method may accept multiple parameters. +The first parameter is always used to pass the base object, ie. `org.acme.Item` in the previous example. +Other parameters are resolved when rendering the template and passed to the extension method. + +.Multiple Parameters Example +[source,java] +---- +@TemplateExtension +class MyExtensions { + + static BigDecimal scale(BigDecimal val, int scale, RoundingMode mode) { <1> + return val.setScale(scale, mode); + } +} +---- +<1> This method matches an expression with base object of the type `BigDecimal.class`, with the `scale` virtual method name and two virtual method parameters. + +[source,html] +---- +{item.discountedPrice.scale(2,mode)} <1> +---- +<1> `item.discountedPrice` is resolved to an instance of `BigDecimal`. + +=== @TemplateData + +A value resolver is automatically generated for a type annotated with `@TemplateData`. +This allows Quarkus to avoid using reflection to access the data at runtime. + +NOTE: Non-public members, constructors, static initializers, static, synthetic and void methods are always ignored. + +[source,java] +---- +package org.acme; + +@TemplateData +class Item { + + public final BigDecimal price; + + public Item(BigDecimal price) { + this.price = price; + } + + public BigDecimal getDiscountedPrice() { + return price.multiply(new BigDecimal("0.9")); + } +} +---- + +Any instance of `Item` can be used directly in the template: + +[source,html] +---- +{#each items} <1> + {it.price} / {it.discountedPrice} +{/each} +---- +<1> `items` is resolved to a list of `org.acme.Item` instances. + +Furthermore, `@TemplateData.properties()` and `@TemplateData.ignore()` can be used to fine-tune the generated resolver. +Finally, it is also possible to specify the "target" of the annotation - this could be useful for third-party classes not controlled by the application: + +[source,java] +---- +@TemplateData(target = BigDecimal.class) +@TemplateData +class Item { + + public final BigDecimal price; + + public Item(BigDecimal price) { + this.price = price; + } +} +---- + +[source,html] +---- +{#each items} <1> + {it.price.setScale(2, rounding)} <1> +{/each} +---- +<1> The generated value resolver knows how to invoke the `BigDecimal.setScale()` method. + +=== RESTEasy Integration + +If you want to use Qute in your JAX-RS application, you'll need to add the `quarkus-resteasy-qute` extension first. +In your `pom.xml` file, add: + +[source,xml] +---- + + io.quarkus + quarkus-resteasy-qute + +---- + +This extension registers a special `ContainerResponseFilter` implementation so that a resource method can return a `TemplateInstance` and the filter takes care of all necessary steps. +A simple JAX-RS resource may look like this: + +.HelloResource.java +[source,java] +---- +package org.acme.quarkus.sample; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Template; + +@Path("hello") +public class HelloResource { + + @Inject + Template hello; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance get(@QueryParam("name") String name) { + return hello.data("name", name); <2> <3> + } +} +---- +<1> If there is no `@ResourcePath` qualifier provided, the field name is used to locate the template. In this particular case, we're injecting a template with path `templates/hello.txt`. +<2> `Template.data()` returns a new template instance that can be customized before the actual rendering is triggered. In this case, we put the name value under the key `name`. The data map is accessible during rendering. +<3> Note that we don't trigger the rendering - this is done automatically by a special `ContainerResponseFilter` implementation. + +==== Variant Templates + +Sometimes it could be useful to render a specific variant of the template based on the content negotiation. +`VariantTemplate` is a perfect match for this use case: + +[source,java] +---- +@Path("/detail") +class DetailResource { + + @Inject + VariantTemplate item; <1> + + @GET + @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN }) + public Rendering item() { + return item.data(new Item("Alpha", 1000)); <2> + } +} +---- +<1> Inject a variant template with base path derived from the injected field - `src/main/resources/templates/item`. +<2> The resulting output depends on the `Accept` header received from the client. For `text/plain` the `src/main/resources/templates/item.txt` template is used. For `text/html` the `META-INF/resources/templates/item.html` template is used. + + +=== Development Mode + +In the development mode, all files located in `src/main/resources/templates` are watched for changes and modifications are immediately visible. + +=== Configuration Reference + +include::{generated-dir}/config/quarkus-qute.adoc[leveloffset=+1, opts=optional] + +== Extension Points + +TODO diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc new file mode 100644 index 0000000000000..2b10e3095e606 --- /dev/null +++ b/docs/src/main/asciidoc/qute.adoc @@ -0,0 +1,273 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Qute Templating Engine +:extension-status: experimental + +include::./attributes.adoc[] + +Qute is a templating engine designed specifically to meet the Quarkus needs. +The usage of reflection is minimized to reduce the size of native images. +The API combines both the imperative and the non-blocking reactive style of coding. +In the development mode, all files located in `src/main/resources/templates` are watched for changes and modifications are immediately visible. +Furthermore, we try to detect most of the template problems at build time. +In this guide, you will learn how to easily render templates in your application. + +include::./status-include.adoc[] + +== Hello World with JAX-RS + +If you want to use Qute in your JAX-RS application, you need to add the `quarkus-qute-resteasy` extension first. +In your `pom.xml` file, add: + +[source,xml] +---- + + io.quarkus + quarkus-resteasy-qute + +---- + +We'll start with a very simple template: + +.hello.txt +[source] +---- +Hello {name}! <1> +---- +<1> `{name}` is a value expression that is evaluated when the template is rendered. + +NOTE: By default, all files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates. Templates are validated during startup and watched for changes in the development mode. + +Now let's inject the "compiled" template in the resource class. + +.HelloResource.java +[source,java] +---- +package org.acme.quarkus.sample; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Template; + +@Path("hello") +public class HelloResource { + + @Inject + Template hello; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance get(@QueryParam("name") String name) { + return hello.data("name", name); <2> <3> + } +} +---- +<1> If there is no `@ResourcePath` qualifier provided, the field name is used to locate the template. In this particular case, we're injecting a template with path `templates/hello.txt`. +<2> `Template.data()` returns a new template instance that can be customized before the actual rendering is triggered. In this case, we put the name value under the key `name`. The data map is accessible during rendering. +<3> Note that we don't trigger the rendering - this is done automatically by a special `ContainerResponseFilter` implementation. + +If your application is running, you can request the endpoint: + +[source, shell] +---- +$ curl -w "\n" http://localhost:8080/hello?name=Martin +Hello Martin! +---- + +== Parameter Declarations and Template Extension Methods + +Qute has many useful features. +In this example, we'll demonstrate two of them. +If you declare a *parameter declaration* in a template then Qute attempts to validate all expressions that reference this parameter and if an incorrect expression is found the build fails. +*Template extension methods* are used to extend the set of accessible properties of data objects. + +Let's suppose we have a simple class like this: + +.Item.java +[source,java] +---- +public class Item { + public String name; + public BigDecimal price; +} +---- + +And we'd like to render a simple HTML page that contains the item name, price and also a discounted price. +The discounted price is sometimes called a "computed property". +We will implement a template extension method to render this property easily. +Let's start again with the template: + +.item.html +[source,html] +---- +{@org.acme.Item item} <1> + + + + +{item.name} <2> + + +

{item.name}

+
Price: {item.price}
+ {#if item.price > 100} <3> +
Discounted Price: {item.discountedPrice}
<4> + {/if} + + +---- +<1> Optional parameter declaration. Qute attempts to validate all expressions that reference the parameter `item`. +<2> This expression is validated. Try to change the expression to `{item.nonSense}` and the build should fail. +<3> `if` is a basic control flow section. +<4> This expression is also validated against the `Item` class and obviously there is no such property declared. However, there is a template extension method declared on the `ItemResource` class - see below. + +Finally, let's create a resource class. + +.ItemResource.java +[source,java] +---- +package org.acme.quarkus.sample; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Template; + +@Path("item") +public class ItemResource { + + @Inject + ItemService service; + + @Inject + Template item; <1> + + @GET + @Path("{id}") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance get(@PathParam("id") Integer id) { + return item.data("item", service.findItem(id)); <2> + } + + @TemplateExtension <3> + static BigDecimal discountedPrice(Item item) { + return item.price.multiply(new BigDecimal("0.9")); + } +} +---- +<1> Inject the template with path `templates/item.html`. +<2> Make the `Item` object accessible in the template. +<3> A static template extension method can be used to add "computed properties" to a data class. The class of the first parameter is used to match the base object and the method name is used to match the property name. + +== Rendering Periodic Reports + +Templating engine could be also very useful when rendering periodic reports. +You'll need to add the `quarkus-scheduler` and `quarkus-qute` extensions first. +In your `pom.xml` file, add: + +[source,xml] +---- + + io.quarkus + quarkus-qute + + + io.quarkus + quarkus-scheduler + +---- + +Let's suppose the have a `SampleService` bean whose `get()` method returns a list of samples. + +.Sample.java +[source,java] +---- +public class Sample { + public boolean valid; + public String name; + public String data; +} +---- + +The template is simple: + +.report.html +[source,html] +---- + + + + +Report {now} + + +

Report {now}

+ {#for sample in samples} <1> +

{sample.name ?: 'Unknown'}

<2> +

+ {#if sample.valid} + {sample.data} + {#else} + Invalid sample found. + {/if} +

+ {/for} + + +---- +<1> The loop section makes it possible to iterate over iterables, maps and streams. +<2> This value expression is using the https://en.wikipedia.org/wiki/Elvis_operator[elvis operator] - if the name is null the default value is used. + +[source,java] +.ReportGenerator.java +---- +package org.acme.quarkus.sample; + +import javax.inject.Inject; + +import io.quarkus.qute.Template; +import io.quarkus.qute.api.ResourcePath; +import io.quarkus.scheduler.Scheduled; + +public class ReportGenerator { + + @Inject + SampleService service; + + @ResourcePath("reports/v1/report_01") <1> + Template report; + + @Scheduled(cron="0 30 * * * ?") <2> + void generate() { + String result = report + .data("samples", service.get()) + .data("now", java.time.LocalDateTime.now()) + .render(); <3> + // Write the result somewhere... + } +} +---- +<1> In this case, we use the `@ResourcePath` qualifier to specify the template path: `templates/reports/v1/report_01.html`. +<2> Use the `@Scheduled` annotation to instruct Quarkus to execute this method on the half hour. For more information see the link:scheduler[Scheduler] guide. +<3> The `TemplateInstance.render()` method triggers rendering. Note that this method blocks the current thread. + +== Qute Reference Guide + +To learn more about Qute, please refer to the link:qute-reference[Qute reference guide]. + +[[qute-configuration-reference]] +== Qute Configuration Reference + +include::{generated-dir}/config/quarkus-qute.adoc[leveloffset=+1, opts=optional] diff --git a/docs/src/main/asciidoc/reactive-messaging.adoc b/docs/src/main/asciidoc/reactive-messaging.adoc index 9a7004e35411d..c56480b3fd3e7 100644 --- a/docs/src/main/asciidoc/reactive-messaging.adoc +++ b/docs/src/main/asciidoc/reactive-messaging.adoc @@ -79,7 +79,7 @@ public class GreetingService { [IMPORTANT] ==== By default, the code consuming the event must be _non-blocking_, as it's called on the Vert.x event loop. -If your processing is blocking, use the `blocking` arttribute: +If your processing is blocking, use the `blocking` attribute: [source, java] ---- @@ -202,7 +202,7 @@ bus.send("address", "hello"); bus.publish("address", "hello"); // Case 3 bus.send("address", "hello, how are you?").thenAccept(message -> { - // reponse + // response }); ---- diff --git a/docs/src/main/asciidoc/reactive-routes.adoc b/docs/src/main/asciidoc/reactive-routes.adoc index 4bd881e7063c5..98a5052cd263e 100644 --- a/docs/src/main/asciidoc/reactive-routes.adoc +++ b/docs/src/main/asciidoc/reactive-routes.adoc @@ -55,14 +55,14 @@ import javax.enterprise.context.ApplicationScoped; @ApplicationScoped <1> public class MyDeclarativeRoutes { - - @Route(path = "/", methods = HttpMethod.GET) <2> - public void handle(RoutingContext rc) { <3> + // neither path nor regex is set - match a path derived from the method name + @Route(methods = HttpMethod.GET) <2> + void hello(RoutingContext rc) { <3> rc.response().end("hello"); } - @Route(path = "/hello", methods = HttpMethod.GET) - public void greetings(RoutingExchange ex) { <4> + @Route(path = "/greetings", methods = HttpMethod.GET) + void greetings(RoutingExchange ex) { <4> ex.ok("hello " + ex.getParam("name").orElse("world")); } } @@ -77,9 +77,8 @@ More details about using the `RoutingContext` is available in the https://vertx. The `@Route` annotation allows to configure: -* The `path` - using the https://vertx.io/docs/vertx-web/java/#_capturing_path_parameters[Vert.x Web format] -* The `regex` - when `path` is not used. -See https://vertx.io/docs/vertx-web/java/#_routing_with_regular_expressions[for more details] +* The `path` - for routing by path, using the https://vertx.io/docs/vertx-web/java/#_capturing_path_parameters[Vert.x Web format] +* The `regex` - for routing with regular expressions, see https://vertx.io/docs/vertx-web/java/#_routing_with_regular_expressions[for more details] * The `methods` - the HTTP verb triggering the route such as `GET`, `POST`... * The `type` - it can be _normal_ (non-blocking), _blocking_ (method dispatched on a worker thread), or _failure_ to indicate that this route is called on failures * The `order` - the order of the route when several routes are involved in handling the incoming request. @@ -109,6 +108,24 @@ public void route(RoutingContext rc) { Each route can use different paths, methods... +=== `@RouteBase` + +This annotation can be used to configure some defaults for reactive routes declared on a class. + +[source,java] +---- +@RouteBase(path = "simple", produces = "text/plain") <1> +public class SimpleRoutes { + + @Route(path = "ping") // the final path is /simple/ping + void ping(RoutingContext rc) { + rc.response().end("pong"); + } +} +---- +<1> The `path` value is used as a prefix for any route method declared on the class where `Route#path()` is used. The `produces` value is used for content-based routing for all routes where `Route#produces()` is empty. + + == Using the Vert.x Web Router You can also register your route directly on the _HTTP routing layer_ by registering routes directly on the `Router` object. diff --git a/docs/src/main/asciidoc/rest-client-multipart.adoc b/docs/src/main/asciidoc/rest-client-multipart.adoc index 13071d8145050..9f5e2c42af61b 100644 --- a/docs/src/main/asciidoc/rest-client-multipart.adoc +++ b/docs/src/main/asciidoc/rest-client-multipart.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc include::./attributes.adoc[] -Resteasy has rich support for the `multipart/*` and `multipart/form-data` mime types. The multipart mime format is used to pass lists of content bodies. Multiple content bodies are embedded in one message. `multipart/form-data` is often found in web application HTML Form documents and is generally used to upload files. The form-data format is the same as other multipart formats, except that each inlined piece of content has a name associated with it. +RESTEasy has rich support for the `multipart/*` and `multipart/form-data` mime types. The multipart mime format is used to pass lists of content bodies. Multiple content bodies are embedded in one message. `multipart/form-data` is often found in web application HTML Form documents and is generally used to upload files. The form-data format is the same as other multipart formats, except that each inlined piece of content has a name associated with it. This guide explains how to use the MicroProfile REST Client with Multipart in order to interact with REST APIs diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 5963c103d40b0..8fcebfa698ef2 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -76,7 +76,7 @@ public class Country { } ---- -The model above in only a subset of the fields provided by the service, but it suffices for the purposes of this guide. +The model above is only a subset of the fields provided by the service, but it suffices for the purposes of this guide. == Create the interface diff --git a/docs/src/main/asciidoc/rest-json.adoc b/docs/src/main/asciidoc/rest-json.adoc index ae3b6bf817d2c..01da7cc416c5a 100644 --- a/docs/src/main/asciidoc/rest-json.adoc +++ b/docs/src/main/asciidoc/rest-json.adoc @@ -60,7 +60,7 @@ Quarkus also supports https://github.com/FasterXML/jackson[Jackson] so, if you p ---- mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ -DprojectGroupId=org.acme \ - -DprojectArtifactId=rest-json \ + -DprojectArtifactId=rest-json-quickstart \ -DclassName="org.acme.rest.json.FruitResource" \ -Dpath="/fruits" \ -Dextensions="resteasy-jackson" @@ -339,7 +339,8 @@ public class LegumeResource { ---- Now let's add a simple web page to display our list of legumes. -In the `src/main/resources/META-INF/resources` directory, add a `legumes.html` file with the content from this https://raw.githubusercontent.com/quarkusio/quarkus-quickstarts/master/rest-json/src/main/resources/META-INF/resources/legumes.html[legumes.html] file in it. +In the `src/main/resources/META-INF/resources` directory, add a `legumes.html` file with the content from this +{quickstarts-blob-url}/rest-json-quickstart/src/main/resources/META-INF/resources/legumes.html[legumes.html] file in it. Open a browser to http://localhost:8080/legumes.html and you will see our list of legumes. @@ -365,6 +366,8 @@ Hopefully, this will change in the future and make the error more obvious. We can register `Legume` for reflection manually by adding the `@RegisterForReflection` annotation on our `Legume` class: [source,JAVA] ---- +import io.quarkus.runtime.annotations.RegisterForReflection; + @RegisterForReflection public class Legume { // ... @@ -465,9 +468,9 @@ In Quarkus, RESTEasy can either run directly on top of the Vert.x HTTP server, o As a result, certain classes, such as `HttpServletRequest` are not always available for injection. Most use-cases for this particular class are covered by JAX-RS equivalents, except for getting the remote client's IP. RESTEasy comes with a replacement API which you can inject: -https://docs.jboss.org/resteasy/docs/4.4.1.Final/javadocs/org/jboss/resteasy/spi/HttpRequest.html[`HttpRequest`], which has the methods -https://docs.jboss.org/resteasy/docs/4.4.1.Final/javadocs/org/jboss/resteasy/spi/HttpRequest.html#getRemoteAddress--[`getRemoteAddress()`] -and https://docs.jboss.org/resteasy/docs/4.4.1.Final/javadocs/org/jboss/resteasy/spi/HttpRequest.html#getRemoteHost--[`getRemoteHost()`] +https://docs.jboss.org/resteasy/docs/4.4.2.Final/javadocs/org/jboss/resteasy/spi/HttpRequest.html[`HttpRequest`], which has the methods +https://docs.jboss.org/resteasy/docs/4.4.2.Final/javadocs/org/jboss/resteasy/spi/HttpRequest.html#getRemoteAddress--[`getRemoteAddress()`] +and https://docs.jboss.org/resteasy/docs/4.4.2.Final/javadocs/org/jboss/resteasy/spi/HttpRequest.html#getRemoteHost--[`getRemoteHost()`] to solve this problem. == What's Different from Jakarta EE Development diff --git a/docs/src/main/asciidoc/scheduler.adoc b/docs/src/main/asciidoc/scheduler.adoc index 25da629f2981c..53d5423077ca6 100644 --- a/docs/src/main/asciidoc/scheduler.adoc +++ b/docs/src/main/asciidoc/scheduler.adoc @@ -10,6 +10,8 @@ include::./attributes.adoc[] Modern applications often need to run specific tasks periodically. In this guide, you learn how to schedule periodic tasks. +TIP: If you need a clustered scheduler use the link:quartz[Quartz extension]. + == Prerequisites To complete this guide, you need: @@ -19,8 +21,6 @@ To complete this guide, you need: * JDK 1.8+ installed with `JAVA_HOME` configured appropriately * Apache Maven 3.5.3+ - - == Architecture In this guide, we create a straightforward application accessible using HTTP to get the current value of a counter. diff --git a/docs/src/main/asciidoc/security-jdbc.adoc b/docs/src/main/asciidoc/security-jdbc.adoc index 158363a2be223..af1a3e607febd 100644 --- a/docs/src/main/asciidoc/security-jdbc.adoc +++ b/docs/src/main/asciidoc/security-jdbc.adoc @@ -271,10 +271,10 @@ quarkus.datasource.driver=org.postgresql.Driver quarkus.datasource.username=quarkus quarkus.datasource.password=quarkus -quarkus.datasource.url=jdbc:postgresql:multiple-data-sources-permissions -quarkus.datasource.driver=org.postgresql.Driver -quarkus.datasource.username=quarkus -quarkus.datasource.password=quarkus +quarkus.datasource.permissions.url=jdbc:postgresql:multiple-data-sources-permissions +quarkus.datasource.permissions.driver=org.postgresql.Driver +quarkus.datasource.permissions.username=quarkus +quarkus.datasource.permissions.password=quarkus quarkus.security.jdbc.enabled=true quarkus.security.jdbc.principal-query.sql=SELECT u.password FROM test_user u WHERE u.username=? diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index 0d3cebce3bd04..c4cff56454e09 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -78,6 +78,7 @@ package org.acme.jwt; import java.security.Principal; import javax.annotation.security.PermitAll; +import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -105,7 +106,7 @@ public class TokenSecuredResource { public String hello(@Context SecurityContext ctx) { // <4> Principal caller = ctx.getUserPrincipal(); <5> String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaims() != null; + boolean hasJWT = jwt.getClaimNames() != null; String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT); return helloReply; // <6> } @@ -264,14 +265,11 @@ For part A of step 1, create a `security-jwt-quickstart/src/main/resources/appli ---- mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem #<1> mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac #<2> - -quarkus.smallrye-jwt.auth-mechanism=MP-JWT # <3> -quarkus.smallrye-jwt.enabled=true # <4> +quarkus.smallrye-jwt.enabled=true #<3> ---- <1> We are setting public key location to point to a classpath publicKey.pem resource location. We will add this key in part B, <>. <2> We are setting the issuer to the URL string `https://quarkus.io/using-jwt-rbac`. -<3> We are setting the authentication mechanism name to MP-JWT. This is not strictly required to allow our quickstart to work, but it is the {mp-jwt} specification standard name for the token based authentication mechanism. -<4> We are enabling the {extension-name}. Also not required since this is the default, +<3> We are enabling the {extension-name}. Also not required since this is the default, but we are making it explicit. === Adding a Public Key @@ -299,33 +297,30 @@ nQIDAQAB === Generating a JWT -Often one obtains a JWT from an identity manager like https://www.keycloak.org/[Keycloak], but for this quickstart we will generate our own using the -https://bitbucket.org/b_c/jose4j/wiki/Home[Jose4J] library and the TokenUtils class shown in the following listing. Take this source and place it into `security-jwt-quickstart/src/test/java/org/acme/jwt/TokenUtils.java`. +Often one obtains a JWT from an identity manager like https://www.keycloak.org/[Keycloak], but for this quickstart we will generate our own using the JWT generation API provided by `smallrye-jwt` and the TokenUtils class shown in the following listing. Take this source and place it into `security-jwt-quickstart/src/test/java/org/acme/jwt/TokenUtils.java`. -NOTE: JWT libraries for many different programming languages can be found at the JWT.io website https://jwt.io/#libraries[JWT Libraries]. .JWT utility class [source, java] ---- package org.acme.jwt; -import java.io.BufferedReader; -import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.Map; -import java.util.stream.Collectors; import org.eclipse.microprofile.jwt.Claims; -import org.jose4j.jws.AlgorithmIdentifiers; -import org.jose4j.jws.JsonWebSignature; -import org.jose4j.jwt.JwtClaims; -import org.jose4j.jwt.NumericDate; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; /** * Utilities for generating a JWT for testing */ @@ -354,34 +349,16 @@ public class TokenUtils { public static String generateTokenString(PrivateKey privateKey, String kid, String jsonResName, Map timeClaims) throws Exception { - JwtClaims claims = JwtClaims.parse(readTokenContent(jsonResName)); + JwtClaimsBuilder claims = Jwt.claims(jsonResName); long currentTimeInSecs = currentTimeInSecs(); - long exp = timeClaims != null && timeClaims.containsKey(Claims.exp.name()) + long exp = timeClaims != null && timeClaims.containsKey(Claims.exp.name()) ? timeClaims.get(Claims.exp.name()) : currentTimeInSecs + 300; - claims.setIssuedAt(NumericDate.fromSeconds(currentTimeInSecs)); - claims.setClaim(Claims.auth_time.name(), NumericDate.fromSeconds(currentTimeInSecs)); - claims.setExpirationTime(NumericDate.fromSeconds(exp)); - - for (Map.Entry entry : claims.getClaimsMap().entrySet()) { - System.out.printf("\tAdded claim: %s, value: %s\n", entry.getKey(), entry.getValue()); - } - - JsonWebSignature jws = new JsonWebSignature(); - jws.setPayload(claims.toJson()); - jws.setKey(privateKey); - jws.setKeyIdHeaderValue(kid); - jws.setHeader("typ", "JWT"); - jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); - - return jws.getCompactSerialization(); - } + claims.issuedAt(currentTimeInSecs); + claims.claim(Claims.auth_time.name(), currentTimeInSecs); + claims.expiresAt(exp); - private static String readTokenContent(String jsonResName) throws IOException { - InputStream contentIS = TokenUtils.class.getResourceAsStream(jsonResName); - try (BufferedReader buffer = new BufferedReader(new InputStreamReader(contentIS))) { - return buffer.lines().collect(Collectors.joining("\n")); - } + return claims.jws().signatureKeyId(kid).sign(privateKey); } /** diff --git a/docs/src/main/asciidoc/security-keycloak-authorization.adoc b/docs/src/main/asciidoc/security-keycloak-authorization.adoc index 4961b34659e8a..3fc9aa0f78b52 100644 --- a/docs/src/main/asciidoc/security-keycloak-authorization.adoc +++ b/docs/src/main/asciidoc/security-keycloak-authorization.adoc @@ -173,9 +173,9 @@ NOTE: By default, applications using the `quarkus-oidc` extension are marked as To start a Keycloak Server you can use Docker and just run the following command: -[source,bash] +[source,bash,subs=attributes+] ---- -docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak +docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8180:8080 {keycloak-docker-image} ---- You should be able to access your Keycloak Server at http://localhost:8180/auth[localhost:8180/auth]. diff --git a/docs/src/main/asciidoc/security-ldap.adoc b/docs/src/main/asciidoc/security-ldap.adoc new file mode 100644 index 0000000000000..7b58f64834efd --- /dev/null +++ b/docs/src/main/asciidoc/security-ldap.adoc @@ -0,0 +1,226 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Using Security with an LDAP Realm + +include::./attributes.adoc[] + +This guide demonstrates how your Quarkus application can use an LDAP server to authenticate and authorize your user identities. + + +== Prerequisites + +To complete this guide, you need: + +* less than 15 minutes +* an IDE +* JDK 1.8+ installed with `JAVA_HOME` configured appropriately +* Apache Maven 3.5.3+ + +== Architecture + +In this example, we build a very simple microservice which offers three endpoints: + +* `/api/public` +* `/api/users/me` +* `/api/admin` + +The `/api/public` endpoint can be accessed anonymously. +The `/api/admin` endpoint is protected with RBAC (Role-Based Access Control) where only users granted with the `adminRole` role can access. At this endpoint, we use the `@RolesAllowed` annotation to declaratively enforce the access constraint. +The `/api/users/me` endpoint is also protected with RBAC (Role-Based Access Control) where only users granted with the `standardRole` role can access. As a response, it returns a JSON document with details about the user. + +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `security-ldap-quickstart` {quickstarts-tree-url}/security-ldap-quickstart[directory]. + +== Creating the Maven Project + +First, we need a new project. Create a new project with the following command: + +[source, subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=security-ldap-quickstart \ + -Dextensions="elytron-security-ldap, resteasy" +cd security-ldap-quickstart +---- + +This command generates a Maven project, importing the `elytron-security-ldap` extension +which is an https://docs.wildfly.org/17/WildFly_Elytron_Security.html#ldap-security-realm[`wildfly-elytron-realm-ldap`] adapter for Quarkus applications. + +== Writing the application + +Let's start by implementing the `/api/public` endpoint. As you can see from the source code below, it is just a regular JAX-RS resource: + +[source,java] +---- +package org.acme.elytron.security.ldap; + +import javax.annotation.security.PermitAll; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/api/public") +public class PublicResource { + + @GET + @PermitAll + @Produces(MediaType.TEXT_PLAIN) + public String publicResource() { + return "public"; + } +} +---- + +The source code for the `/api/admin` endpoint is also very simple. The main difference here is that we are using a `@RolesAllowed` annotation to make sure that only users granted with the `adminRole` role can access the endpoint: + + +[source,java] +---- +package org.acme.elytron.security.ldap; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/api/admin") +public class AdminResource { + + @GET + @RolesAllowed("adminRole") + @Produces(MediaType.TEXT_PLAIN) + public String adminResource() { + return "admin"; + } +} +---- + +Finally, let's consider the `/api/users/me` endpoint. As you can see from the source code below, we are trusting only users with the `standardRole` role. +We are using `SecurityContext` to get access to the current authenticated Principal and we return the user's name. This information is loaded from the ldap server. + +[source,java] +---- +package org.acme.elytron.security.ldap; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.SecurityContext; + +@Path("/api/users") +public class UserResource { + + @GET + @RolesAllowed("standardRole") + @Path("/me") + @Produces(MediaType.APPLICATION_JSON) + public String me(@Context SecurityContext securityContext) { + return securityContext.getUserPrincipal().getName(); + } +} +---- + +=== Configuring the Application + +[source,properties] +-- +quarkus.security.ldap.enabled=true + +quarkus.security.ldap.dir-context.principal=uid=tool,ou=accounts,o=YourCompany,c=DE +quarkus.security.ldap.dir-context.url=ldaps://ldap.server.local +quarkus.security.ldap.dir-context.password=PASSWORD + +quarkus.security.ldap.identity-mapping.rdn-identifier=uid +quarkus.security.ldap.identity-mapping.search-base-dn=ou=users,ou=tool,o=YourCompany,c=DE + +quarkus.security.ldap.identity-mapping.attribute-mappings."0".from=cn +quarkus.security.ldap.identity-mapping.attribute-mappings."0".to=groups +quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter=(member=uid={0}) +quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=roles,ou=tool,o=YourCompany,c=DE +-- + +The `elytron-security-ldap` extension requires a dir-context and an identity-mapping with at least one attribute-mapping to authenticate the user and its identity. + +== Testing the Application + +The application is now protected and the identities are provided by our LDAP server. +The very first thing to check is to ensure the anonymous access works. + +[source,shell] +---- +$ curl -i -X GET http://localhost:8080/api/public +HTTP/1.1 200 OK +Content-Length: 6 +Content-Type: text/plain;charset=UTF-8 + +public% +---- + +Now, let's try a to hit a protected resource anonymously. + +[source,shell] +---- +$ curl -i -X GET http://localhost:8080/api/admin +HTTP/1.1 401 Unauthorized +Content-Length: 14 +Content-Type: text/html;charset=UTF-8 + +Not authorized% +---- + +So far so good, now let's try with an allowed user. + +[source,shell] +---- +$ curl -i -X GET -u adminUser:adminUserPassword http://localhost:8080/api/admin +HTTP/1.1 200 OK +Content-Length: 5 +Content-Type: text/plain;charset=UTF-8 + +admin% +---- +By providing the `adminUser:adminUserPassword` credentials, the extension authenticated the user and loaded their roles. +The `adminUser` user is authorized to access to the protected resources. + +The user `adminUser` should be forbidden to access a resource protected with `@RolesAllowed("standardRole")` because it doesn't have this role. +[source,shell] +---- +$ curl -i -X GET -u adminUser:adminUserPassword http://localhost:8080/api/users/me +HTTP/1.1 403 Forbidden +Content-Length: 34 +Content-Type: text/html;charset=UTF-8 + +Forbidden% +---- + +Finally, using the user `standardUser` works and the security context contains the principal details (username for instance). +[source,shell] +---- +curl -i -X GET -u standardUser:standardUserPassword http://localhost:8080/api/users/me +HTTP/1.1 200 OK +Content-Length: 4 +Content-Type: text/plain;charset=UTF-8 + +user% +---- + +[[configuration-reference]] +== Configuration Reference + +include::{generated-dir}/config/quarkus-elytron-security-ldap.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/security-oauth2.adoc b/docs/src/main/asciidoc/security-oauth2.adoc index f9e1689e48508..53f3b7ddc738b 100644 --- a/docs/src/main/asciidoc/security-oauth2.adoc +++ b/docs/src/main/asciidoc/security-oauth2.adoc @@ -7,6 +7,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc include::./attributes.adoc[] :extension-name: Elytron Security OAuth2 +:extension-status: preview This guide explains how your Quarkus application can utilize OAuth2 tokens to provide secured access to the JAX-RS endpoints. @@ -15,6 +16,8 @@ It can be used to implement an application authentication mechanism based on tok If your OAuth2 Authentication server provides JWT tokens, you should use link:security-jwt[MicroProfile JWT RBAC] instead, this extension aims to be used with opaque tokens and validate the token by calling an introspection endpoint. +include::./status-include.adoc[] + == Solution We recommend that you follow the instructions in the next sections and create the application step by step. diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 94aa09c318592..2edd3432a6d41 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -83,9 +83,9 @@ all paths are being protected by a policy that ensures that only `authenticated` To start a Keycloak Server you can use Docker and just run the following command: -[source,bash] +[source,bash,subs=attributes+] ---- -docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:7.0.0 +docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8180:8080 {keycloak-docker-image} ---- You should be able to access your Keycloak Server at http://localhost:8180/auth[localhost:8180/auth]. diff --git a/docs/src/main/asciidoc/security-openid-connect.adoc b/docs/src/main/asciidoc/security-openid-connect.adoc index 4e46737879d1c..7cd42ecd3b7b0 100644 --- a/docs/src/main/asciidoc/security-openid-connect.adoc +++ b/docs/src/main/asciidoc/security-openid-connect.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc = Quarkus - Using OpenID Connect Adapter to Protect JAX-RS Applications include::./attributes.adoc[] +:extension-status: preview This guide demonstrates how your Quarkus application can use an OpenID Connect Adapter to protect your JAX-RS applications using bearer token authorization, where these tokens are issued by OpenId Connect and OAuth 2.0 compliant Authorization Servers such as https://www.keycloak.org/about.html[Keycloak]. @@ -13,6 +14,8 @@ Bearer Token Authorization is the process of authorizing HTTP requests based on We are going to give you a guideline on how to use OpenId Connect and OAuth 2.0 in your JAX-RS applications using the Quarkus OpenID Connect Extension. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -160,9 +163,9 @@ If you plan to consume this application from another application running on a di To start a Keycloak Server you can use Docker and just run the following command: -[source,bash] +[source,bash,subs=attributes+] ---- -docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:7.0.0 +docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8180:8080 {keycloak-docker-image} ---- You should be able to access your Keycloak Server at http://localhost:8180/auth[localhost:8180/auth]. diff --git a/docs/src/main/asciidoc/security.adoc b/docs/src/main/asciidoc/security.adoc index 6f8f6056ba505..2bb02bd08efff 100644 --- a/docs/src/main/asciidoc/security.adoc +++ b/docs/src/main/asciidoc/security.adoc @@ -29,7 +29,7 @@ in order for Quarkus to know how to find the authentication information to check |Provides support for OAuth2 flows using Elytron. This extension will likely be deprecated soon and replaced by a reactive Vert.x version. |link:security-jwt[quarkus-smallrye-jwt] -|A Microprofile JWT implementation that provides support for authenticating using Json Web Tokens. This also allows you to inject the token and claims into the application as per the MP JWT spec. +|A MicroProfile JWT implementation that provides support for authenticating using Json Web Tokens. This also allows you to inject the token and claims into the application as per the MP JWT spec. |link:security-openid-connect[quarkus-oidc] |Provides support for authenticating via an OpenID Connect provider such as Keycloak. @@ -68,7 +68,8 @@ The following properties can be used to configure form based auth: include::{generated-dir}/config/quarkus-vertx-http-config-group-form-auth-config.adoc[opts=optional, leveloffset=+1] -## Authorization in REST endpoints and CDI beans using annotations +[#standard-security-annotations] +== Authorization in REST endpoints and CDI beans using annotations Quarkus comes with built-in security to allow for Role-Based Access Control (link:https://en.wikipedia.org/wiki/Role-based_access_control[RBAC]) based on the common security annotations `@RolesAllowed`, `@DenyAll`, `@PermitAll` on REST endpoints and CDI beans. diff --git a/docs/src/main/asciidoc/software-transactional-memory.adoc b/docs/src/main/asciidoc/software-transactional-memory.adoc index 439dfc505b053..cd77b95685e06 100644 --- a/docs/src/main/asciidoc/software-transactional-memory.adoc +++ b/docs/src/main/asciidoc/software-transactional-memory.adoc @@ -6,23 +6,83 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc = Using Software Transactional Memory in Quarkus include::./attributes.adoc[] - -Quarkus supports the Software Transactional Memory (STM) implementation provided by the -Narayana open source project. Narayana STM allows a program to group object accesses into -a transaction such that other transactions either see all of the changes at once or they -see none of them. - -STM offers an approach to developing transactional applications in a highly concurrent -environment with some of the same characteristics of ACID (Atomicity, Consistency, -Isolation and Durability) transactions. - -To use Narayana STM you must define which objects you would like to be transactional using java -annotations. Please refer to the https://narayana.io/docs/project/index.html#d0e16066[Narayana STM manual] -and the https://narayana.io//docs/project/index.html#d0e16133[STM annotations guide] for more details. +:extension-status: preview + +Software Transactional Memory (STM) has been around in research environments since the late +1990's and has relatively recently started to appear in products and various programming +languages. We won't go into all of the details behind STM but the interested reader could look at https://groups.csail.mit.edu/tds/papers/Shavit/ShavitTouitou-podc95.pdf[this paper]. +However, suffice it to say that STM offers an approach to developing transactional applications in a highly +concurrent environment with some of the same characteristics of ACID transactions, which you've probably already used +through JTA. Importantly though, the Durability property is relaxed (removed) within STM implementations, +or at least made optional. This is not the situation with JTA, where state changes are made durable +to a relational database which supports https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf[the X/Open XA +standard]. + +Note, the STM implementation provided by Quarkus is based on the https://narayana.io/docs/project/index.html#d0e16066[Narayana STM] implementation. This document isn't meant to be a replacement for that project's documentation so you may want +to look at that for more detail. However, we will try to focus more on how you can combine some of the key capabilities +into Quarkus when developing Kubernetes native applications and microservices. + +== Why use STM with Quarkus? + +Now you may still be asking yourself "Why STM instead of JTA?" or "What are the benefits +to STM that I don't get from JTA?" Let's try to answer those or similar questions, with +a particular focus on why we think they're great for Quarkus, microservices and Kubernetes +native applications. So in no specific order ... + +* The goal of STM is to simplify object reads and writes from multiple threads/protect +state from concurrent updates. The Quarkus STM implementation will safely manage any conflicts between +these threads using whatever isolation model has been chosen to protect that specific state +instance (object in the case of Quarkus). In Quarkus STM, there are two isolation implementations, +pessimistic (the default), which would cause conflicting threads to be blocked until the original +has completed its updates (committed or aborted the transaction); then there's the optimistic +approach which allows all of the threads to proceed and checks for conflicts at commit time, where +one or more of the threads may be forced to abort if there have been conflicting updates. + +* STM objects have state but it doesn't need to be persistent (durable). In fact the +default behaviour is for objects managed within transactional memory to be volatile, such that +if the service or microservice within which they are being used crashes or is spawned elsewhere, e.g., +by a scheduler, all state in memory is lost and the objects start from scratch. But surely you get this and more +with JTA (and a suitable transactional datastore) and don't need to worry about restarting your application? +Not quite. There's a trade-off here: we're doing away +with persistent state and the overhead of reading from and then writing (and sync-ing) to the datastore during each +transaction. This makes updates to (volatile) state very fast but you still get the benefits of atomic updates +across multiple STM objects (e.g., objects your team wrote then calling objects you inherited from another team and requiring +them to make all-or-nothing updates), as well as consistency +and isolation in the presence of concurrent threads/users (common in distributed microservices architectures). +Furthermore, not all stateful applications need to be durable - even when JTA transactions are used, it tends to be the +exception and not the rule. And as you'll see later, because applications can optionally start and control transactions, it's possible to build microservices which can undo state changes and try alternative paths. + +* Another benefit of STM is composability and modularity. You can write concurrent Quarkus objects/services that +can be easily composed with any other services built using STM, without exposing the details of how the objects/services +are implemented. As we discussed earlier, this ability to compose objects you wrote with those other teams may have +written weeks, months or years earlier, and have A, C and I properties can be hugely beneficial. Furthermore, some +STM implementations, including the one Quarkus uses, support nested transactions and these allow changes made within +the context of a nested (sub) transaction to later be rolled back by the parent transaction. + +* Although the default for STM object state is volatile, it is possible to configure the STM implementation +such that an object's state is durable. Although it's possible to configure Narayana such that different +backend datastores can be used, including relational databases, the default is the local operating system +file system, which means you don't need to configure anything else with Quarkus such as a database. + +* Many STM implementations allow "plain old language objects" to be made STM-aware with little or no changes to +the application code. You can build, test and deploy applications without wanting them to be STM-aware and +then later add those capabilities if they become necessary and without much development overhead at all. + +== Building STM applications There is also a fully worked example in the quickstarts which you may access by cloning the Git repository: `git clone {quickstarts-clone-url}`, or by downloading an {quickstarts-archive-url}[archive]. -Look for the `software-transactional-memory-quickstart` example. +Look for the `software-transactional-memory-quickstart` example. This will help to understand how you +can build STM-aware applications with Quarkus. However, before we do so there are a few basic concepts +which we need to cover. + +Note, as you will see, STM in Quarkus relies on a number of annotations to define behaviours. The lack +of these annotations causes sensible defaults to be assumed but it is important for the developer to +understand what these may be. Please refer to the https://narayana.io/docs/project/index.html#d0e16066[Narayana STM manual] +and the https://narayana.io//docs/project/index.html#d0e16133[STM annotations guide] for more details on +all of the annotations Narayana STM provides. + +include::./status-include.adoc[] == Setting it up @@ -40,12 +100,17 @@ To use the extension include it as a dependency in your application pom: -- -Now you may use the STM library just like you would normally use it. But briefly, the process is: +== Defining STM-aware classes -== Defining Transactional Objects +In order for the STM subsytem to have knowledge about which classes are to be managed within the context +of transactional memory it is necessary to provide a minimal level of instrumentation. This occurs by +categorising STM-aware and STM-unaware classes through an interface boundary; specifically all STM-aware objects +must be instances of classes which inherit from interfaces that themselves have been annotated to identify them +as STM-aware. Any other objects (and their classes) which do not follow this rule will not be managed by the +STM subsystem and hence any of their state changes will not be rolled back, for example. -Transactional objects must implement an interface. Add the `org.jboss.stm.annotations.Transactional` annotation to the -interfaces that you wish to be managed by a transactional container. For example +The specific annotation that STM-aware application interfaces must use is `org.jboss.stm.annotations.Transactional`. +For example: [source,java] -- @@ -56,15 +121,48 @@ public interface FlightService { } -- -Unless specified using other annotations, all public methods of implementations of this object will be assumed to modify the state of the object. -Please refer to the Narayana guide for details of how to exert finer grained control over the transactional behaviour of objects that implement -interfaces marked with the `@Transactional` annotation. +Classes which implement this interface are able to use additional annotations from Narayana to tell the STM +subsystem about things such as whether a method will modify the state of the object, or what state variables +within the class should be managed transactionally, e.g., some instance variables may not need to be rolled back +if a transaction aborts. As mentioned earlier, if those annotations are not present then defaults are chosen to +guarantee safety, such as assuming all methods will modify state. + +[source,java] +-- +public class FlightServiceImpl implements FlightService { + @ReadLock + public int getNumberOfBookings() { ... } + public void makeBooking(String details) {...} + + @NotState + private int timesCalled; +} +-- + +For example, by using the `@ReadLock` annotation on the `getNumberOfBookings` method, we are able to tell the +STM subsystem that no state modifications will occur in this object when it is used in the transactional +memory. Also, the `@NotState` annotation tells the system to ignore `timesCalled` when transactions commit or +abort, so this value only changes due to application code. + +Please refer to the Narayana guide for details of how to exert finer grained control over the transactional +behaviour of objects that implement interfaces marked with the `@Transactional` annotation. == Creating STM objects -The STM library needs to be told which objects it should be managing by providing a container for the transactional memory. -The default container (`org.jboss.stm.Container`) provides support for volatile objects that cannot be shared between JVMs -(although other constructors do allow for other models): +The STM subsystem needs to be told about which objects it should be managing. The Quarkus (aka Narayana) STM implementation +does this by providing containers of transactional memory within which these object instances reside. Until an object +is placed within one of these STM containers it cannot be managed within transactions and any state changes will +not possess the A, C, I (or even D) properties. + +Note, the term "container" was defined within the STM implementation years before Linux containers came along. It may +be confusing to use especially in a Kubernetes native environment such as Quarkus, but hopefully +the reader can do the mental mapping. + +The default STM container (`org.jboss.stm.Container`) provides support for volatile objects that can only be shared between +threads in the same microservice/JVM instance. When a STM-aware object is placed into the container it returns a handle +through which that object should then be used in the future. It is important to use this handle as continuing to access +the object through the original reference will not allow the STM subsystem to track access and manage state and +concurrency control. [source,java] -- @@ -79,25 +177,30 @@ The default container (`org.jboss.stm.Container`) provides support for volatile <1> You need to tell each Container about the type of objects for which it will be responsible. In this example it will be instances that implement the FlightService interface. -<2> Then you create an instance that implements FlightService. You cannot use it directly at this stage because - its operations aren't being monitored by the Container. -<3> To obtain a managed instance, pass the instance to the `container` to obtain a reference through which you - will be able perform transactional operations. This reference can be used safely from multiple threads - (provided that each thread uses it in a transaction context - see the next section. +<2> Then you create an instance that implements `FlightService`. You should not use it directly at this stage because + access to it is not being managed by the STM subsystem. +<3> To obtain a managed instance, pass the original object to the STM `container` which then returns a reference + through which you will be able perform transactional operations. This reference can be used safely from multiple threads. == Defining transaction boundaries -STM objects must be accessed within a transaction (otherwise they will behave just like any other java object). -You can define the transaction boundary in two ways: +Once an object is placed within an STM container the application developer can manage the scope of transactions +within which it is used. There are some annotations which can be applied to the STM-aware class to have the +container automatically create a transaction whenever a specific method is invoked. === Declarative approach -The easiest, but less flexible, way to define your transaction boundaries is to place an `@NestedTopLevel` or `@Nested` annotation on the interface. -Then when a method of your transactional object is invoked a new transaction will be created for the duration of the method call. +If the `@NestedTopLevel` or `@Nested` annotation is placed on a method signature then the STM container will +start a new transaction when that method is invoked and attempt to commit it when the method returns. If there is +a transaction already associated with the calling thread then each of these annotations behaves slightly differently: +the former annotation will always create a new top-level transaction within which the method will execute, so the enclosing +transaction does not behave as a parent, i.e., the nested top-level transaction will commit or abort independently; the +latter annotation will create a transaction with is properly nested within the calling transaction, i.e., that +transaction acts as the parent of this newly created transaction. -=== API approach +=== Programmatic approach -The more flexible approach is to manually start a transaction before accessing methods of transactional objects: +The application can programmatically start a transaction before accessing the methods of STM objects: [source,java] -- @@ -130,19 +233,9 @@ aa.begin(); <2> provides a number of features for managing conflicting behaviour and these are covered in the Narayana STM manual). <6> Programmatically decide to abort the transaction which means that the changes made by the flight and taxi services are discarded. -== Concurrency behaviour - -The goal of STM is to simplify object reads and writes from multiple threads. -Threads are responsible for starting their own transactions before accessing -a transactional object. The STM library will safely manage any conflicts between -these threads. For example, if the access mode is pessimistic (the default), -and a thread enters a transactional method then other threads may be blocked -until the first thread leaves the method. This blocking behaviour may be modified -using suitable STM annotations. Optimistic concurrency is also supported: in -this mode conflicts are checked only at commit time and a thread may be forced -to abort if another thread has made conflicting updates. +== Distributed transactions -Remark: sharing a transaction between multiple threads is possible but is currently +Sharing a transaction between multiple services is possible but is currently an advanced use case only and the Narayana documentation should be consulted if this behaviour is required. In particular, STM does not yet support the features described in the link:context-propagation[Context Propagation guide]. diff --git a/docs/src/main/asciidoc/spring-data-jpa.adoc b/docs/src/main/asciidoc/spring-data-jpa.adoc index e762f9a28b1b6..13490a1b83dd6 100644 --- a/docs/src/main/asciidoc/spring-data-jpa.adoc +++ b/docs/src/main/asciidoc/spring-data-jpa.adoc @@ -6,10 +6,13 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc = Quarkus - Extension for Spring Data API include::./attributes.adoc[] +:extension-status: preview While users are encouraged to use Hibernate ORM with Panache for Relational Database access, Quarkus provides a compatibility layer for Spring Data JPA repositories in the form of the `spring-data-jpa` extension. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -596,3 +599,4 @@ Quarkus support has more Spring compatibility features. See the following guides * link:spring-di[Quarkus - Extension for Spring DI] * link:spring-web[Quarkus - Extension for Spring Web] +* link:spring-security[Quarkus - Extension for Spring Security] diff --git a/docs/src/main/asciidoc/spring-di.adoc b/docs/src/main/asciidoc/spring-di.adoc index 9e3ae38038faf..0e081803c9e93 100644 --- a/docs/src/main/asciidoc/spring-di.adoc +++ b/docs/src/main/asciidoc/spring-di.adoc @@ -6,11 +6,14 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc = Quarkus - Quarkus Extension for Spring DI API include::./attributes.adoc[] +:extension-status: preview While users are encouraged to use CDI annotations for injection, Quarkus provides a compatibility layer for Spring dependency injection in the form of the `spring-di` extension. This guide explains how a Quarkus application can leverage the well known Dependency Injection annotations included in the Spring Framework. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -306,3 +309,4 @@ Quarkus supports additional Spring compatibility features. See the following gui * link:spring-web[Quarkus - Extension for Spring Web] * link:spring-data-jpa[Quarkus - Extension for Spring Data JPA] +* link:spring-security[Quarkus - Extension for Spring Security] diff --git a/docs/src/main/asciidoc/spring-security.adoc b/docs/src/main/asciidoc/spring-security.adoc new file mode 100644 index 0000000000000..19c3aedba4060 --- /dev/null +++ b/docs/src/main/asciidoc/spring-security.adoc @@ -0,0 +1,413 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Quarkus Extension for Spring Security API + +include::./attributes.adoc[] +:extension-status: preview + +While users are encouraged to use <>, Quarkus provides a compatibility layer for Spring Security in the form of the `spring-security` extension. + +This guide explains how a Quarkus application can leverage the well known Spring Security annotations to define authorizations on RESTful services using roles. + +include::./status-include.adoc[] + +== Prerequisites + +To complete this guide, you need: + +* less than 15 minutes +* an IDE +* JDK 1.8+ installed with `JAVA_HOME` configured appropriately +* Apache Maven 3.5.3+ +* Some familiarity with the Spring Web extension + + +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `spring-security-quickstart` {quickstarts-tree-url}/spring-security-quickstart[directory]. + +== Creating the Maven project + +First, we need a new project. Create a new project with the following command: + +[source,shell,subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=spring-security-quickstart \ + -DclassName="org.acme.spring.security.GreetingController" \ + -Dpath="/greeting" \ + -Dextensions="spring-web, spring-security, quarkus-elytron-security-properties-file" +cd spring-security-quickstart +---- + +This command generates a Maven project with a REST endpoint and imports the `spring-web`, `spring-security` and `security-properties-file` extensions. + +For more information about `security-properties-file` you can check the guide of link:security-properties[quarkus-elytron-security-properties-file] extension. + +== GreetingController + +The Quarkus Maven plugin automatically generated a controller with the Spring Web annotations to define our REST endpoint (instead of the JAX-RS ones used by default). +The `src/main/java/org/acme/spring/web/GreetingController.java` file looks as follows: + +[source,java] +---- +package org.acme.spring.security; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PathVariable; + +@RestController +@RequestMapping("/greeting") +public class GreetingController { + + @GetMapping + public String hello() { + return "hello"; + } +} +---- + +== GreetingControllerTest + +Note that a test for the controller has been created as well: + +[source, java] +---- +package org.acme.spring.security; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class GreetingControllerTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("hello")); + } + +} +---- + +== Package and run the application + +Run the application with: `./mvn quarkus:dev`. +Open your browser to http://localhost:8080/greeting. + +The result should be: `{"message": "hello"}`. + +== Modify the controller to secure the `hello` method + +In order to restrict access to the `hello` method to users with certain roles, the `@Secured` annotation will be utilized. +The updated controller will be: + +[source,java] +---- +package org.acme.spring.security; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PathVariable; + +@RestController +@RequestMapping("/greeting") +public class GreetingController { + + @Secured("admin") + @GetMapping + public String hello() { + return "hello"; + } +} +---- + +The easiest way to setup users and roles for our example is to use the `security-properties-file` extension. This extension essentially allows users and roles to be defined in the main Quarkus configuration file - `application.properties`. +For more information about this extension check link:security-properties.adoc[the associated guide]. +An example configuration would be the following: + +[source,properties] +---- +quarkus.security.users.embedded.enabled=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.scott=jb0ss +quarkus.security.users.embedded.roles.scott=admin,user +quarkus.security.users.embedded.users.stuart=test +quarkus.security.users.embedded.roles.stuart=user +---- + +Note that the test also needs to be updated. It could look like: + +== GreetingControllerTest + +[source, java] +---- +package org.acme.spring.security; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class GreetingControllerTest { + + @Test + public void testHelloEndpointForbidden() { + given().auth().preemptive().basic("stuart", "test") + .when().get("/greeting") + .then() + .statusCode(403); + } + + @Test + public void testHelloEndpoint() { + given().auth().preemptive().basic("scott", "jb0ss") + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("hello")); + } + +} +---- + +== Test the changes + +Access allowed:: + +Open your browser again to http://localhost:8080/greeting and introduce `scott` and `jb0ss` in the dialog displayed. ++ +The word `hello` should be displayed. + +Access forbidden:: + +Open your browser again to http://localhost:8080/greeting and let empty the dialog displayed. ++ +The result should be: ++ +[source] +---- +Access to localhost was denied +You don't have authorization to view this page. +HTTP ERROR 403 +---- + +== Run the application as a native executable + +You can of course create a native image using the instructions of the link:building-native-image[Building a native executable guide]. + + +== Supported Spring Security functionalities + +Quarkus currently only supports a subset of the functionalities that Spring Security provides with more features being planned. More specifically, Quarkus supports the security related features of role-based authorization semantics +(think of `@Secured` instead of `@RolesAllowed`). + +=== Annotations + +The table below summarizes the supported annotations: + +.Supported Spring Security annotations +|=== +|Name|Comments + +|@Secured +| + +|@PreAuthorize +|See next section for more details + +|=== + +==== @PreAuthorize + +Quarkus provides support for some of the most used features of Spring Security's `@PreAuthorize` annotation. +The expressions that are supported are the following: + +hasRole:: ++ +To test if the current user has a specific role, the `hasRole` expression can be used inside `@PreAuthorize`. ++ +Some examples are: `@PreAuthorize("hasRole('admin')")`, `@PreAuthorize("hasRole(@roles.USER)")` where the `roles` is a bean that could be defined like so: ++ +[source, java] +---- +import org.springframework.stereotype.Component; + +@Component +public class Roles { + + public final String ADMIN = "admin"; + public final String USER = "user"; +} +---- + +hasAnyRole:: + +In the same fashion as `hasRole`, users can use `hasAnyRole` to check if the logged in user has any of the specified roles. ++ +Some examples are: `@PreAuthorize("hasAnyRole('admin')")`, `@PreAuthorize("hasAnyRole(@roles.USER, 'view')")` + +permitAll:: Adding `@PreAuthorize("permitAll()")` to a method will ensure that that method is accessible by any user (including anonymous users). Adding it to a class will ensure that all public methods +of the class that are not annotated with any other Spring Security annotation will be accessible. + +denyAll:: Adding `@PreAuthorize("denyAll()")` to a method will ensure that that method is not accessible by any user. Adding it to a class will ensure that all public methods +of the class that are not annotated with any other Spring Security annotation will not be accessible to any user. + +isAnonymous:: When annotating a bean method with `@PreAuthorize("isAnonymous()")` the method will only be accessible if the current user is anonymous - i.e. a non logged in user. + +isAuthenticated:: When annotating a bean method with `@PreAuthorize("isAuthenticated()")` the method will only be accessible if the current user is a logged in user. Essentially the +method is only unavailable for anonymous users. + +#paramName == authentication.principal.username:: This syntax allows users to check if a parameter (or a field of the parameter) of the secured method is equal to the logged in username. ++ +Examples of this use case are: ++ +[source,java] +---- +public class Person { + + private final String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} + +@Component +public class MyComponent { + + @PreAuthorize("#username == authentication.principal.username") <1> + public void doSomething(String username, String other){ + + } + + @PreAuthorize("#person.name == authentication.principal.username") <2> + public void doSomethingElse(Person person){ + + } +} +---- +<1> `doSomething` can be executed if the current logged in user is the same as the `username` method parameter +<2> `doSomethingElse` can be executed if the current logged in user is the same as the `name` field of `person` method parameter ++ +TIP: the use of `authentication.` is optional, so using `principal.username` has the same result. + +#paramName != authentication.principal.username:: This is similar to the previous expression with the difference being that the method parameter must be different than the logged in username. + +@beanName.method():: This syntax allows developers to specify that the execution of method of a specific bean will determine if the current user can access the secured method. ++ +The syntax is best explained with an example. +Let's assume that a `MyComponent` bean has been created like so: ++ +[source,java] +---- +@Component +public class MyComponent { + + @PreAuthorize("@personChecker.check(#person, authentication.principal.username)") + public void doSomething(Person person){ + + } +} +---- ++ +The `doSomething` method has been annotated with `@PreAuthorize` using an expression that indicates that method `check` of a bean named `personChecker` needs +to be invoked to determine whether the current user is authorized to invoke the `doSomething` method. ++ +An example of the `PersonChecker` could be: ++ +[source,java] +---- +@Component +public class PersonChecker { + + @Override + public boolean check(Person person, String username) { + return person.getName().equals(username); + } +} +---- ++ +Note that for the `check` method the parameter types must match what is specified in `@PreAuthorize` and that the return type must be a `boolean`. + +===== Combining expressions + +The `@PreAuthorize` annotations allows for the combination of expressions using logical `AND` / `OR`. Currently there is a limitation where only a single +logical operation can be used (meaning mixing `AND` and `OR` isn't allowed). + +Some examples of allowed expressions are: + +[source,java] +---- + + @PreAuthorize("hasAnyRole('user', 'admin') AND #user == principal.username") + public void allowedForUser(String user) { + + } + + @PreAuthorize("hasRole('user') OR hasRole('admin')") + public void allowedForUserOrAdmin() { + + } + + @PreAuthorize("hasAnyRole('view1', 'view2') OR isAnonymous() OR hasRole('test')") + public void allowedForAdminOrAnonymous() { + + } +---- + +Also to be noted that currently parentheses are not supported and expressions are evaluated from left to right when needed. + +== Important Technical Note + +Please note that the Spring support in Quarkus does not start a Spring Application Context nor are any Spring infrastructure classes run. +Spring classes and annotations are only used for reading metadata and / or are used as user code method return types or parameter types. +What that means for end users, is that adding arbitrary Spring libraries will not have any effect. Moreover Spring infrastructure +classes (like `org.springframework.beans.factory.config.BeanPostProcessor` for example) will not be executed. + +== Conversion Table + +The following table shows how Spring Security annotations can be converted to JAX-RS annotations. + +|=== +|Spring |JAX-RS |Comments + +|@Secured("admin") +|@RolesAllowed("admin") +| + +|=== + +== More Spring guides + +Quarkus support has more Spring compatibility features. See the following guides for more details: + +* link:spring-di[Quarkus - Extension for Spring DI] +* link:spring-web[Quarkus - Extension for Spring Web] +* link:spring-data-jpa[Quarkus - Extension for Spring Data JPA] + + diff --git a/docs/src/main/asciidoc/spring-web.adoc b/docs/src/main/asciidoc/spring-web.adoc index 5153b9c662950..311fb9c6f258e 100644 --- a/docs/src/main/asciidoc/spring-web.adoc +++ b/docs/src/main/asciidoc/spring-web.adoc @@ -6,11 +6,14 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc = Quarkus - Quarkus Extension for Spring Web API include::./attributes.adoc[] +:extension-status: preview While users are encouraged to use JAX-RS annotation for defining REST endpoints, Quarkus provides a compatibility layer for Spring Web in the form of the `spring-web` extension. This guide explains how a Quarkus application can leverage the well known Spring Web annotations to define RESTful services. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -318,5 +321,5 @@ Quarkus support has more Spring compatibility features. See the following guides * link:spring-di[Quarkus - Extension for Spring DI] * link:spring-data-jpa[Quarkus - Extension for Spring Data JPA] - +* link:spring-security[Quarkus - Extension for Spring Security] diff --git a/docs/src/main/asciidoc/status-include.adoc b/docs/src/main/asciidoc/status-include.adoc new file mode 100644 index 0000000000000..a594e251362e8 --- /dev/null +++ b/docs/src/main/asciidoc/status-include.adoc @@ -0,0 +1,20 @@ +[NOTE] +==== +This technology is considered {extension-status}. + +ifeval::["{extension-status}" == "experimental"] +In _experimental_ mode, early feedback is requested to mature the idea. +There is no guarantee of stability nor long term presence in the platform until the solution matures. +Feedback is welcome on our https://groups.google.com/d/forum/quarkus-dev[mailing list] or as issues in our https://github.com/quarkusio/quarkus/issues[GitHub issue tracker]. +endif::[] +ifeval::["{extension-status}" == "preview"] +In _preview_, backward compatibility and presence in the ecosystem is not guaranteed. +Specific improvements might require to change configuration or APIs and plans to become _stable_ are under way. +Feedback is welcome on our https://groups.google.com/d/forum/quarkus-dev[mailing list] or as issues in our https://github.com/quarkusio/quarkus/issues[GitHub issue tracker]. +endif::[] +ifeval::["{extension-status}" == "stable"] +Being _stable_, backward compatibility and presence in the ecosystem are taken very seriously. +endif::[] + +For a full list of possible extension statuses, check our https://quarkus.io/faq/#extension-status[FAQ entry]. +==== diff --git a/docs/src/main/asciidoc/stylesheet/config.css b/docs/src/main/asciidoc/stylesheet/config.css index 90dc54fcf58ed..76d10fa97bb2a 100644 --- a/docs/src/main/asciidoc/stylesheet/config.css +++ b/docs/src/main/asciidoc/stylesheet/config.css @@ -1,6 +1,7 @@ table.configuration-reference.tableblock { border-collapse: separate; border-spacing: 1px; + border: none; } table.configuration-reference.tableblock thead th.tableblock { @@ -21,7 +22,6 @@ table.configuration-reference.tableblock tbody tr th { border: none; border-bottom: 1px solid #4695eb; vertical-align: bottom; - color: white; } table.configuration-reference.tableblock tbody tr:first-child th { @@ -37,7 +37,7 @@ table.configuration-reference.tableblock tbody tr td:nth-child(3) { table.configuration-reference.tableblock tbody tr th:nth-child(2) p, table.configuration-reference.tableblock tbody tr th:nth-child(3) p { font-weight: normal; - color: white; + color: black; } table.configuration-reference.tableblock tbody tr th p { @@ -46,6 +46,7 @@ table.configuration-reference.tableblock tbody tr th p { table.configuration-reference.tableblock tbody tr td { padding-left: 30px; + border: none; } table.configuration-reference.tableblock tbody tr td > .content > .paragraph .icon { @@ -107,6 +108,7 @@ table.configuration-reference.tableblock td.tableblock > .content > :last-child } input#config-search-0 { + -webkit-appearance: none; display: block; width: 100%; margin-top: 10px; @@ -114,7 +116,7 @@ input#config-search-0 { transition: transform 250ms ease-in-out; font-size: 14px; line-height: 18px; - color: #4695eb; + color: color(#4695eb a(0.8)); background-color: transparent; background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath stroke='white' fill='white' d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -124,11 +126,6 @@ input#config-search-0 { transition: all 250ms ease-in-out; backface-visibility: hidden; transform-style: preserve-3d; -} - -input#config-search-0 { - color: color(#4695eb a(0.8)); - text-transform: uppercase; letter-spacing: 1.5px; } @@ -154,4 +151,3 @@ table.configuration-reference.tableblock .description-decoration i { font-size: 0.7rem; color: #4695eb; } - diff --git a/docs/src/main/asciidoc/validation.adoc b/docs/src/main/asciidoc/validation.adoc index 31282a625355f..a381216dc1a60 100644 --- a/docs/src/main/asciidoc/validation.adoc +++ b/docs/src/main/asciidoc/validation.adoc @@ -128,7 +128,7 @@ Let's now create the `Result` class as an inner class: [source, java] ---- -private class Result { +public class Result { Result(String message) { this.success = true; diff --git a/docs/src/main/asciidoc/vault.adoc b/docs/src/main/asciidoc/vault.adoc index c425646154249..5e5695c6642b0 100644 --- a/docs/src/main/asciidoc/vault.adoc +++ b/docs/src/main/asciidoc/vault.adoc @@ -10,6 +10,7 @@ include::./attributes.adoc[] :vault-version: 1.2.2 :root-token: s.5VUS8pte13RqekCB2fmMT3u2 :client-token: s.s93BVzJPzBiIGuYJHBTkG8Uw +:extension-status: preview https://www.vaultproject.io/[HashiCorp Vault] is a multi-purpose tool aiming at protecting sensitive data, such as credentials, certificates, access tokens, encryption keys, ... In the context of Quarkus, @@ -23,6 +24,8 @@ https://www.vaultproject.io/docs/secrets/kv/index.html[Vault kv secret engine] a Under the hood, the Quarkus Vault extension takes care of authentication when negotiating a client Vault token plus any transparent token or lease renewals according to ttl and max-ttl. +include::./status-include.adoc[] + == Prerequisites To complete this guide, you need: @@ -419,6 +422,21 @@ quarkus.datasource.credentials-provider = mydatabase quarkus.hibernate-orm.database.generation=drop-and-create ---- +[NOTE] +==== +Another way to specify the datasource password is with property indirection. Assuming that vault path +`myapps/vault-quickstart/config` contains key `my-db-password`, all is required on the datasource configuration is: +``` +quarkus.datasource.username = sarah +quarkus.datasource.password = ${my-db-password} +``` +The only drawback is that the password will never be fetched again from vault after the initial property loading. +This means that if the db password was changed while running, the application would have to be restarted after +vault has been updated with the new password. +This contrasts with the credentials provider approach, which fetches the password from vault everytime a connection +creation is attempted. +==== + Restart the application after rebuilding it, and test it with the new endpoint: [source,shell] diff --git a/docs/src/main/asciidoc/vertx.adoc b/docs/src/main/asciidoc/vertx.adoc index ff99fb4a05b30..d6ce0c9f596fa 100644 --- a/docs/src/main/asciidoc/vertx.adoc +++ b/docs/src/main/asciidoc/vertx.adoc @@ -158,7 +158,7 @@ Now let's use the Vert.x API to implement the artificially delayed reply with th // Delay reply by 10ms vertx.setTimer(10, l -> { // Compute elapsed time in milliseconds - long duration = MILLISECONDS.convert(System.nanoTime() - start, NANOSECONDS); + long duration = TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS); // Format message String message = String.format("Hello %s! (%d ms)%n", name, duration); diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc old mode 100644 new mode 100755 index 0778a25b40296..89d4dfc22e93e --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -198,7 +198,7 @@ Get a CDI portable extension running:: The CDI portable extension model is very flexible. Too flexible to benefit from the build time boot promoted by Quarkus. Most extension we have seen do not make use of these extreme flexibilities capabilities. -The way to port a CDI extension to Quarkus is to rewrite it as a Quarkus extension which will define the various beans at build time (deployment time in extension parley). +The way to port a CDI extension to Quarkus is to rewrite it as a Quarkus extension which will define the various beans at build time (deployment time in extension parlance). == Technical aspect @@ -220,7 +220,7 @@ Static Init:: This means that if a framework can boot in this phase then it will have its booted state directly written to the image, and so the boot code does not need to be executed when the image is started. + -There are some restrictions on what can be done in this stage as the Substrate VM disallows some objects in the native executable. For example you should not attempt to listen on a port or start threads in this phase. +There are some restrictions on what can be done in this stage as the Substrate VM disallows some objects in the native executable. For example you should not attempt to listen on a port or start threads in this phase. In addition, it is disallowed to read run time configuration during static initialization. + In non-native pure JVM mode, there is no real difference between Static and Runtime Init, except that Static Init is always executed first. This mode benefits from the same build phase augmentation as native mode as the descriptor parsing and annotation scanning are done at build time and any associated class/framework dependencies can be removed from the build output jar. In servers like @@ -378,7 +378,24 @@ The above sequence of commands does the following: A Maven build performed immediately after generating the modules should fail due to a `fail()` assertion in one of the test classes. There is one step (specific to the Quarkus source tree) that you should do manually when creating a new extension: -Add the extension coordinates to `devtools/common/src/main/filtered/extensions.json`. +create a `quarkus-extension.yaml` file that describe your extension inside the runtime module `src/main/resources/META-INF` folder. + +This is the `quarkus-extension.yaml` of the `quarkus-agroal` extension, you can use it as an example: + +[source,yaml] +---- +name: "Agroal - Database connection pool" +metadata: + keywords: + - "agroal" + - "database-connection-pool" + - "datasource" + - "jdbc" + guide: "https://quarkus.io/guides/datasource" + categories: + - "data" + status: "stable" +---- Note that the parameters of the mojo that will be constant for all the extensions added to this source tree are configured in `extensions/pom.xml` so that they do not need to be passed on the command line each time a new extension is added: @@ -530,7 +547,7 @@ public ServiceWriterBuildItem registerOneService() { * providers of multiple configuration-related services. */ @BuildStep -public void registerSeveralSerivces( +public void registerSeveralServices( BuildProducer providerProducer ) { providerProducer.produce(new ServiceWriterBuildItem( @@ -616,7 +633,7 @@ conceptual goal that does not have a concrete representation. * causing this step to be run. */ @BuildStep -@Produces(NativeImageBuildItem.class) +@Produce(NativeImageBuildItem.class) void produceNativeImage() { // ... // (produce the native image) @@ -632,7 +649,7 @@ void produceNativeImage() { * an instance of {@code SomeOtherBuildItem}. */ @BuildStep -@Consumes(NativeImageBuildItem.class) +@Consume(NativeImageBuildItem.class) SomeOtherBuildItem secondBuildStep() { return new SomeOtherBuildItem("foobar"); } @@ -674,7 +691,7 @@ A build step may produce values for subsequent steps in several possible ways: - By returning a <> or <> instance - By returning a `List` of a multi build item class - By injecting a `BuildProducer` of a simple or multi build item class -- By annotating the method with `@io.quarkus.deployment.annotations.Produces`, giving the class name of a +- By annotating the method with `@io.quarkus.deployment.annotations.Produce`, giving the class name of a <> If a simple build item is declared on a build step, it _must_ be produced during that build step, otherwise an error @@ -694,7 +711,7 @@ A build step may consume values from previous steps in the following ways: - By injecting a <> - By injecting an `Optional` of a simple build item class - By injecting a `List` of a <> class -- By annotating the method with `@io.quarkus.deployment.annotations.Consumes`, giving the class name of a +- By annotating the method with `@io.quarkus.deployment.annotations.Consume`, giving the class name of a <> Normally it is an error for a step which is included to consume a simple build item that is not produced by any other @@ -852,11 +869,37 @@ In addition, custom converters may be registered by adding their fully qualified Though these implicit converters use reflection, Quarkus will automatically ensure that they are loaded at the appropriate time. +===== Optional Values + +If the configuration type is one of the optional types, then empty values are allowed for the configuration key; otherwise, +specification of an empty value will result in a configuration error which prevents the application from starting. This +is especially relevant to configuration properties of inherently emptiable values such as `List`, `Set`, and `String`. Such +value types will never be empty; in the event of an empty value, an empty `Optional` is always used. + +==== Configuration Default Values + +A configuration item can be marked to have a default value. The default value is used when no matching configuration key +is specified in the configuration. + +Configuration items with a primitive type (such as `int` or `boolean`) implicitly use a default value of `0` or `false`. The +sole exception to this rule is the `char` type which does not have an implicit default value. + +A property with a default value is not implicitly optional. If a non-optional configuration item with a default value +is explicitly specified to have an empty value, the application will report a configuration error and will not start. If +it is desired for a property to have a default value and also be optional, it must have an `Optional` type as described above. + ==== Configuration Groups Configuration values are always collected into grouping classes which are marked with the `@io.quarkus.runtime.annotations.ConfigGroup` annotation. These classes contain a field for each key within its group. In addition, configuration groups can be nested. +===== Optional Configuration Groups + +A nested configuration group may be wrapped with an `Optional` type. In this case, the group is not populated unless one +or more properties within that group are specified in the configuration. If the group is populated, then any required +properties in the group must also be specified otherwise a configuration error will be reported and the application will +not start. + ==== Configuration Maps A `Map` can be used for configuration at any position where a configuration group would be allowed. The key type of such a @@ -886,6 +929,7 @@ this change is complete. ===== Configuration Root Phases +Configuration roots are strictly bound by configuration phase, and attempting to access a configuration root from outside of its corresponding phase will result in an error. A configuration root dictates when its contained keys are read from configuration, and when they are available to applications. The phases defined by `io.quarkus.runtime.annotations.ConfigPhase` are as follows: [cols="<3m,^1,^1,^1,^1,<8",options="header"] @@ -1246,6 +1290,143 @@ HealthBuildItem addHealthCheck(AgroalBuildTimeConfig agroalBuildTimeConfig) { } ---- +=== Extension Metrics + +An extension can decide to provide metrics through the `quarkus-smallrye-metrics` extension. +A typical use case for this would be that an extension scans the application code for relevant components (like entities, messaging endpoints, etc.) +and creates a set of metrics for each of these components. A unified mechanism for metric registration is provided via the `MetricBuildItem` class +provided by the `quarkus-smallrye-metrics-spi` module. + +There are several distinct situations that can occur and each requires slightly different handling: + +1. The underlying library used by your extension is using the MicroProfile Metrics API directly. +2. The underlying library uses its own way for collecting metrics and makes them available at runtime using its own API. +3. The underlying library does not provide metrics (or there is no library at all) and you need to insert some code in the extension's codebase that will collect the metrics. + +What is common for all cases is that the extension should have a build-time config property that enables metrics exposure for the extension, +it should be named `quarkus..metrics.enabled` and be `false` by default. + +==== Case 1: The library uses MP Metrics +If the library exposes metrics by itself, we don't have to do much. However, there are a few points to consider: + +- It should be possible to disable all metrics for an extension, which can be a bit problematic if the library registers them directly +rather than through the unified registration mechanism in the `quarkus-smallrye-metrics-spi` module. Therefore the library should contain +a way to turn all metrics off, for example using a library-specific system property that can be set during build time (by emitting a `SystemPropertyBuildItem`), +and will disable metrics if at least one of the properties `quarkus..metrics.enabled` and `quarkus.smallrye-metrics.extensions.enabled` is `false`. +- It is desirable to be able to omit the MP Metrics dependency at runtime, so if possible, the library should be written in a way that +it will still work when the MP Metrics dependencies (or at least the implementation, `io.smallrye:smallrye-metrics`) are unavailable. This can be achieved by +wrapping all code that does something metric-related into an `if` condition that checks whether metrics integration is enabled. If the library performs injections +of the `MetricRegistry`, which is not avoidable by introducing an `if` condition, it will unfortunately not be possible to remove the `microprofile-metrics` dependency, +but it is still possible to avoid the need for `smallrye-metrics` (which contains the implementation class of the metric registry) by introducing a custom "no-op" +implementation of the `MetricRegistry` which can, for example, return `null` from all its methods. This no-op implementation should be added to the application's classes +so that it can be injected instead of the regular `io.smallrye.metrics.MetricRegistryImpl`. An example of a no-op implementation can be found at +https://github.com/quarkusio/quarkus/blob/master/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/NoopMetricRegistry.java[NoopMetricRegistry] + +==== Case 2: The library provides its own metric API +In this case, the extension can make use of the `quarkus-smallrye-metrics-spi` module to expose the metrics from the library using MP Metrics. + +You should do the following: + +- Import the `quarkus-smallrye-metrics-spi` library in your deployment module. +- Import the `quarkus-smallrye-metrics` extension as an **optional** dependency in your runtime module so it will not impact the size of the application if +metrics are not included. +- In your processor, produce any number of `MetricBuildItem` items as shown in the example below. +The metric name of all metrics should start with a short name of the extension that owns them. +- Pass the value of `quarkus..metrics.enabled` to the constructor of `MetricBuildItem` objects that the extension produces and if it is false, the metric will be ignored. + +An example build step for build-time registration of metrics: + +[source%nowrap.java] +---- +@BuildStep +void registerMetrics(BuildProducer metrics) { + Metadata activeCountMetadata = Metadata.builder() + .withName("agroal.active.count") + .withDescription("Count of active connections") + .withType(MetricType.GAUGE) + .build(); + List dataSources = getAllDataSources(); // your relevant components specific to the extension + for(DataSource dataSource : dataSources) { + metrics.produce(new MetricBuildItem(activeCountMetadata, + new ActiveCountGauge(dataSource.getName()), + agroalBuildTimeConfig.metricsEnabled, + configRootName, // name of the config root pertaining to this extension + new Tag("datasource", dataSource.getName()))); + } +} +---- + +In this example, there is a gauge per each data source that shows the current number of active connections. +In this example, we provide an object that implements the logic of the metric, in the case of a gauge, +it needs to implement `org.eclipse.microprofile.metrics.Gauge`. Example: + +[source%nowrap.java] +---- +public class ActiveCountGauge implements Gauge { + + private String dataSourceName; + + public ActiveCountGauge() { + + } + + public ActiveCountGauge(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + @Override + public Long getValue() { + DataSource ds = obtainDataSource(dataSourceName); // some logic to get hold of the data source + return ds.getMetrics().getActiveCount(); + } +} +---- + +When registering gauges, it is required to provide a custom object that implements the correct metric type. +For metric types other than gauges, this is optional - you can choose whether to provide an implementation, +or whether the default implementation class from `SmallRye Metrics` should be instantiated for the metric and registered +in the metric registry. To update the metric value, it will be necessary to look them up in the metric registry +and call the methods specific to each metric type. + +Look into the `AgroalProcessor#registerMetrics` method for an example how this was done for the Agroal extension. + +==== Case 3: It is necessary to collect metrics within the extension code + +In this case, the dependency setup and build-time registration is the same as in case 2. The difference will be that instead of introducing +custom metric objects that bridge between library-specific metrics and MP Metrics, the extension's code will contain metric collection code like this: + +[source%nowrap.java] +---- +public void methodThatShouldBeCounted() { + if(metricsEnabled) { + Counter counter = MetricRegistries.get(MetricRegistry.Type.VENDOR).counter("name"); + counter.inc(); + } + // proceed with the invocation normally +} +---- + +In this case, be aware that the MP Metrics API and implementation might not be available on the runtime classpath! +This is why there is a `if(metricsEnabled)` check that should make sure that if metrics are not enabled, we don't touch MP Metrics classes, +because that would fail. The extension itself will need to provide such check, and it must return true only if both of these conditions hold: + +- The Metrics capability is present and therefore the SmallRye Metrics library is available +- Metrics are not disabled for this extension + +The recommended way to implement this check would be to produce a specific `SystemPropertyBuildItem` during build, which will make it easy +to evaluate these two conditions, and then check back to this system property at runtime. Example: + +---- +@BuildStep +SystemPropertyBuildItem produceMetricsEnabledProperty(AgroalBuildTimeConfig extensionSpecificConfig, + Capabilities capabilities) { + return new SystemPropertyBuildItem("metrics.enabled", + String.valueOf(capabilities.isCapabilityPresent(Capabilities.METRICS) && + extensionSpecificConfig.metricsEnabled)); +} +---- + + === Customizing JSON handling from an extension Extensions often need to register serializers and/or deserializers for types the extension provides. @@ -1598,6 +1779,9 @@ executable. Some of these build items are listed below: `io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem`:: Includes static resources into the native executable. +`io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem`:: + Includes directory's static resources into the native executable. + `io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem`:: A class that will be reinitialized at runtime by Substrate. This will result in the static initializer running twice. @@ -1616,9 +1800,12 @@ A class that will be initialized at runtime rather than build time. This will ca `io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem`:: A convenience feature that allows you to control most of the above features from a single build item. -`io.quarkus.deployment.builditem.NativeEnableAllCharsetsBuildItem`:: +`io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem`:: Indicates that all charsets should be enabled in native image. +`io.quarkus.deployment.builditem.NativeImageEnableAllTimeZonesBuildItem`:: +Indicates that all timezones should be enabled in native image. + `io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem`:: A convenient way to tell Quarkus that the extension requires SSL and it should be enabled during native image build. When using this feature, remember to add your extension to the list of extensions that offer SSL support automatically on the https://github.com/quarkusio/quarkus/blob/master/docs/src/main/asciidoc/native-and-ssl.adoc[native and ssl guide]. @@ -1717,7 +1904,7 @@ cd rest-json ---- ==== Bean Defining Annotations -The CDI layer processes CDI beans that are either explicitly registered or that it discovers based on bean defining annotations as defined in http://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#bean_defining_annotations">[2.5.1. Bean defining annotations]. You can expand this set of annotations to include annotations your extension processes using a `BeanDefiningAnnotationBuildItem` as shown in this `TestProcessor#registerBeanDefinningAnnotations` example: +The CDI layer processes CDI beans that are either explicitly registered or that it discovers based on bean defining annotations as defined in http://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#bean_defining_annotations[2.5.1. Bean defining annotations]. You can expand this set of annotations to include annotations your extension processes using a `BeanDefiningAnnotationBuildItem` as shown in this `TestProcessor#registerBeanDefinningAnnotations` example: .Register a Bean Definining Annotation [source,java] @@ -2055,16 +2242,16 @@ public final class MyExtProcessor { @BuildStep void registerNativeImageReources(BuildProducer services) { String service = "META-INF/services/" + io.quarkus.SomeService.class.getName(); - + // find out all the implementation classes listed in the service files - Set implementations = + Set implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), service); - // register every listed implementation class so they can be instantiated + // register every listed implementation class so they can be instantiated // in native-image at run-time services.produce( - new ServiceProviderBuildItem(io.quarkus.SomeService.class.getName(), + new ServiceProviderBuildItem(io.quarkus.SomeService.class.getName(), implementations.toArray(new String[0]))); } } @@ -2086,13 +2273,13 @@ public final class MyExtProcessor { void registerNativeImageReources(BuildProducer resource, BuildProducer reflectionClasses) { String service = "META-INF/services/" + io.quarkus.SomeService.class.getName(); - + // register the service file so it is visible in native-image resource.produce(new NativeImageResourceBuildItem(service)); - - // register every listed implementation class so they can be inspected/instantiated + + // register every listed implementation class so they can be inspected/instantiated // in native-image at run-time - Set implementations = + Set implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), service); reflectionClasses.produce( @@ -2104,7 +2291,7 @@ public final class MyExtProcessor { While this is the easiest way to get your services running natively, it's less efficient than scanning the implementation classes at build time and generating code that registers them at static-init time instead of relying on reflection. -You can achieve that by adapting the previous build step to use a static-init recorder instead of registering +You can achieve that by adapting the previous build step to use a static-init recorder instead of registering classes for reflection: [source,java] @@ -2113,19 +2300,19 @@ public final class MyExtProcessor { @BuildStep @Record(ExecutionTime.STATIC_INIT) - void registerNativeImageReources(RecorderContext recorderContext, + void registerNativeImageReources(RecorderContext recorderContext, SomeServiceRecorder recorder) { String service = "META-INF/services/" + io.quarkus.SomeService.class.getName(); - + // read the implementation classes Collection> implementationClasses = new LinkedHashSet<>(); Set implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), service); for(String implementation : implementations) { - implementationClasses.add((Class) + implementationClasses.add((Class) recorderContext.classProxy(implementation)); } - + // produce a static-initializer with those classes recorder.configure(implementationClasses); } diff --git a/docs/src/main/java/io/quarkus/docs/generation/AllConfigGenerator.java b/docs/src/main/java/io/quarkus/docs/generation/AllConfigGenerator.java index eef2d4ee3861c..9d01af74e2266 100644 --- a/docs/src/main/java/io/quarkus/docs/generation/AllConfigGenerator.java +++ b/docs/src/main/java/io/quarkus/docs/generation/AllConfigGenerator.java @@ -21,12 +21,11 @@ import org.eclipse.aether.resolution.ArtifactRequest; import org.eclipse.aether.resolution.ArtifactResult; -import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import io.quarkus.annotation.processor.generate_doc.ConfigDocGeneratedOutput; import io.quarkus.annotation.processor.generate_doc.ConfigDocItem; import io.quarkus.annotation.processor.generate_doc.ConfigDocItemScanner; import io.quarkus.annotation.processor.generate_doc.ConfigDocSection; @@ -37,8 +36,7 @@ import io.quarkus.docs.generation.ExtensionJson.Extension; public class AllConfigGenerator { - public static void main(String[] args) - throws AppModelResolverException, JsonParseException, JsonMappingException, IOException { + public static void main(String[] args) throws AppModelResolverException, IOException { if (args.length != 2) { // exit 1 will break Maven throw new IllegalArgumentException("Usage: "); @@ -165,7 +163,7 @@ public static void main(String[] args) for (Map.Entry> entry : sortedConfigItemsByExtension.entrySet()) { final List configDocItems = entry.getValue(); // sort the items - ConfigDocWriter.sort(configDocItems); + DocGeneratorUtil.sort(configDocItems); // insert a header ConfigDocSection header = new ConfigDocSection(); header.setSectionDetailsTitle(entry.getKey()); @@ -177,19 +175,29 @@ public static void main(String[] args) } // write our docs - configDocWriter.writeAllExtensionConfigDocumentation(allItems); + ConfigDocGeneratedOutput allConfigGeneratedOutput = new ConfigDocGeneratedOutput("all-config.adoc", true, allItems, + false); + configDocWriter.writeAllExtensionConfigDocumentation(allConfigGeneratedOutput); } private static String guessExtensionNameFromDocumentationFileName(String docFileName) { // sanitise - if (docFileName.startsWith("quarkus-")) + if (docFileName.startsWith("quarkus-")) { docFileName = docFileName.substring(8); - if (docFileName.endsWith(".adoc")) + } + + if (docFileName.endsWith(".adoc")) { docFileName = docFileName.substring(0, docFileName.length() - 5); - if (docFileName.endsWith("-config")) + } + + if (docFileName.endsWith("-config")) { docFileName = docFileName.substring(0, docFileName.length() - 7); - if (docFileName.endsWith("-configuration")) + } + + if (docFileName.endsWith("-configuration")) { docFileName = docFileName.substring(0, docFileName.length() - 14); + } + docFileName = docFileName.replace('-', ' '); return capitalize(docFileName); } @@ -224,4 +232,4 @@ private static void collectConfigRoots(ZipFile zf, Extension extension, Map\u203a " +heading: + align: left + font_color: $base_font_color + font_style: bold + # h1 is used for part titles (book doctype) or the doctitle (article doctype) + h1_font_size: floor($base_font_size * 2.6) + # h2 is used for chapter titles (book doctype only) + h2_font_size: floor($base_font_size * 2.15) + h3_font_size: round($base_font_size * 1.7) + h4_font_size: $base_font_size_large + h5_font_size: $base_font_size + h6_font_size: $base_font_size_small + #line_height: 1.4 + # correct line height for Noto Serif metrics (comes with built-in line height) + line_height: 1 + margin_top: $vertical_rhythm * 0.4 + margin_bottom: $vertical_rhythm * 0.9 + min_height_after: $base_line_height_length * 1.5 +title_page: + align: right + logo: + top: 10% + title: + top: 55% + font_size: $heading_h1_font_size + font_color: 999999 + line_height: 0.9 + subtitle: + font_size: $heading_h3_font_size + font_style: bold_italic + line_height: 1 + authors: + margin_top: $base_font_size * 1.25 + font_size: $base_font_size_large + font_color: 181818 + revision: + margin_top: $base_font_size * 1.25 +block: + margin_top: 0 + margin_bottom: $vertical_rhythm +caption: + align: left + font_size: $base_font_size * 0.95 + font_style: italic + # FIXME perhaps set line_height instead of / in addition to margins? + margin_inside: $vertical_rhythm / 3 + #margin_inside: $vertical_rhythm / 4 + margin_outside: 0 +lead: + font_size: $base_font_size_large + line_height: 1.4 +abstract: + font_color: 5C6266 + font_size: $lead_font_size + line_height: $lead_line_height + font_style: italic + first_line_font_style: bold + title: + align: center + font_color: $heading_font_color + font_size: $heading_h4_font_size + font_style: $heading_font_style +admonition: + column_rule_color: $base_border_color + column_rule_width: $base_border_width + padding: [0, $horizontal_rhythm, 0, $horizontal_rhythm] + #icon: + # tip: + # name: far-lightbulb + # stroke_color: 111111 + # size: 24 + label: + text_transform: uppercase + font_style: bold +blockquote: + font_size: $base_font_size_large + border_color: $base_border_color + border_width: 0 + border_left_width: 5 + # FIXME disable negative padding bottom once margin collapsing is implemented + padding: [0, $horizontal_rhythm, $block_margin_bottom * -0.75, $horizontal_rhythm + $blockquote_border_left_width / 2] + cite_font_size: $base_font_size_small + cite_font_color: 999999 +verse: + font_size: $blockquote_font_size + border_color: $blockquote_border_color + border_width: $blockquote_border_width + border_left_width: $blockquote_border_left_width + padding: $blockquote_padding + cite_font_size: $blockquote_cite_font_size + cite_font_color: $blockquote_cite_font_color +# code is used for source blocks (perhaps change to source or listing?) +code: + font_color: $base_font_color + font_family: $literal_font_family + font_size: ceil($base_font_size) + padding: $code_font_size + line_height: 1.25 + # line_gap is an experimental property to control how a background color is applied to an inline block element + line_gap: 3.8 + background_color: F5F5F5 + border_color: CCCCCC + border_radius: $base_border_radius + border_width: 0.75 +conum: + font_family: $literal_font_family + font_color: $literal_font_color + font_size: $base_font_size + line_height: 4 / 3 + glyphs: circled +example: + border_color: $base_border_color + border_radius: $base_border_radius + border_width: 0.75 + background_color: FFFFFF + # FIXME reenable padding bottom once margin collapsing is implemented + padding: [$vertical_rhythm, $horizontal_rhythm, 0, $horizontal_rhythm] +image: + align: left +prose: + margin_top: $block_margin_top + margin_bottom: $block_margin_bottom +sidebar: + background_color: EEEEEE + border_color: E1E1E1 + border_radius: $base_border_radius + border_width: $base_border_width + # FIXME reenable padding bottom once margin collapsing is implemented + padding: [$vertical_rhythm, $vertical_rhythm * 1.25, 0, $vertical_rhythm * 1.25] + title: + align: center + font_color: $heading_font_color + font_size: $heading_h4_font_size + font_style: $heading_font_style +thematic_break: + border_color: $base_border_color + border_style: solid + border_width: $base_border_width + margin_top: $vertical_rhythm * 0.5 + margin_bottom: $vertical_rhythm * 1.5 +description_list: + term_font_style: bold + term_spacing: $vertical_rhythm / 4 + description_indent: $horizontal_rhythm * 1.25 +outline_list: + indent: $horizontal_rhythm * 1.5 + #marker_font_color: 404040 + # NOTE outline_list_item_spacing applies to list items that do not have complex content + item_spacing: $vertical_rhythm / 2 +table: + background_color: $page_background_color + border_color: DDDDDD + border_width: $base_border_width + cell_padding: 3 + head: + font_style: bold + border_bottom_width: $base_border_width * 2.5 + body: + stripe_background_color: F9F9F9 + foot: + background_color: F0F0F0 +toc: + indent: $horizontal_rhythm + line_height: 1.4 + dot_leader: + #content: ". " + font_color: A9A9A9 + #levels: 2 3 +footnotes: + font_size: round($base_font_size * 0.75) + item_spacing: $outline_list_item_spacing / 2 +header: + font_size: $base_font_size_small + line_height: 1 + vertical_align: middle +footer: + font_size: $base_font_size_small + # NOTE if background_color is set, background and border will span width of page + border_color: DDDDDD + border_width: 0.25 + height: $base_line_height_length * 2.5 + line_height: 1 + padding: [$base_line_height_length / 2, 1, 0, 1] + vertical_align: top + recto: + #columns: "<50% =0% >50%" + right: + content: '{page-number}' + verso: + #columns: $footer_recto_columns + left: + content: $footer_recto_right_content diff --git a/docs/src/main/resources/theme/fonts/LICENSE-Overpass.txt b/docs/src/main/resources/theme/fonts/LICENSE-Overpass.txt new file mode 100644 index 0000000000000..b26cc66815618 --- /dev/null +++ b/docs/src/main/resources/theme/fonts/LICENSE-Overpass.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 by Red Hat, Inc. All rights reserved. +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/src/main/resources/theme/fonts/LICENSE-mplus-testflight-58.txt b/docs/src/main/resources/theme/fonts/LICENSE-mplus-testflight-58.txt new file mode 100644 index 0000000000000..7e8ad05046921 --- /dev/null +++ b/docs/src/main/resources/theme/fonts/LICENSE-mplus-testflight-58.txt @@ -0,0 +1,16 @@ +M+ FONTS Copyright (C) 2002-2014 M+ FONTS PROJECT + +- + +LICENSE_E + + + + +These fonts are free software. +Unlimited permission is granted to use, copy, and distribute them, with +or without modification, either commercially or noncommercially. +THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY. + + +http://mplus-fonts.sourceforge.jp/mplus-outline-fonts/ diff --git a/docs/src/main/resources/theme/fonts/Overpass-Bold.ttf b/docs/src/main/resources/theme/fonts/Overpass-Bold.ttf new file mode 100644 index 0000000000000..363d6517c2a41 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/Overpass-Bold.ttf differ diff --git a/docs/src/main/resources/theme/fonts/Overpass-BoldItalic.ttf b/docs/src/main/resources/theme/fonts/Overpass-BoldItalic.ttf new file mode 100644 index 0000000000000..707c5a5191dc0 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/Overpass-BoldItalic.ttf differ diff --git a/docs/src/main/resources/theme/fonts/Overpass-Italic.ttf b/docs/src/main/resources/theme/fonts/Overpass-Italic.ttf new file mode 100644 index 0000000000000..20bc2a07e9355 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/Overpass-Italic.ttf differ diff --git a/docs/src/main/resources/theme/fonts/Overpass-Regular-conums.ttf b/docs/src/main/resources/theme/fonts/Overpass-Regular-conums.ttf new file mode 100644 index 0000000000000..92f9f77fec45e Binary files /dev/null and b/docs/src/main/resources/theme/fonts/Overpass-Regular-conums.ttf differ diff --git a/docs/src/main/resources/theme/fonts/Overpass-Regular.ttf b/docs/src/main/resources/theme/fonts/Overpass-Regular.ttf new file mode 100644 index 0000000000000..298a9a323cbd4 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/Overpass-Regular.ttf differ diff --git a/docs/src/main/resources/theme/fonts/OverpassMono-Bold.ttf b/docs/src/main/resources/theme/fonts/OverpassMono-Bold.ttf new file mode 100644 index 0000000000000..58e459672dbca Binary files /dev/null and b/docs/src/main/resources/theme/fonts/OverpassMono-Bold.ttf differ diff --git a/docs/src/main/resources/theme/fonts/OverpassMono-Regular-conums.ttf b/docs/src/main/resources/theme/fonts/OverpassMono-Regular-conums.ttf new file mode 100644 index 0000000000000..3720203256b0d Binary files /dev/null and b/docs/src/main/resources/theme/fonts/OverpassMono-Regular-conums.ttf differ diff --git a/docs/src/main/resources/theme/fonts/OverpassMono-Regular.ttf b/docs/src/main/resources/theme/fonts/OverpassMono-Regular.ttf new file mode 100644 index 0000000000000..67bdb9a8dbe28 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/OverpassMono-Regular.ttf differ diff --git a/docs/src/main/resources/theme/fonts/mplus1mn-bold-ascii.ttf b/docs/src/main/resources/theme/fonts/mplus1mn-bold-ascii.ttf new file mode 100644 index 0000000000000..726bcc46931d3 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/mplus1mn-bold-ascii.ttf differ diff --git a/docs/src/main/resources/theme/fonts/mplus1mn-bold_italic-ascii.ttf b/docs/src/main/resources/theme/fonts/mplus1mn-bold_italic-ascii.ttf new file mode 100644 index 0000000000000..c91d944a179b5 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/mplus1mn-bold_italic-ascii.ttf differ diff --git a/docs/src/main/resources/theme/fonts/mplus1mn-italic-ascii.ttf b/docs/src/main/resources/theme/fonts/mplus1mn-italic-ascii.ttf new file mode 100644 index 0000000000000..77c16844c92ac Binary files /dev/null and b/docs/src/main/resources/theme/fonts/mplus1mn-italic-ascii.ttf differ diff --git a/docs/src/main/resources/theme/fonts/mplus1mn-regular-ascii-conums.ttf b/docs/src/main/resources/theme/fonts/mplus1mn-regular-ascii-conums.ttf new file mode 100644 index 0000000000000..5645bbeb3ee8b Binary files /dev/null and b/docs/src/main/resources/theme/fonts/mplus1mn-regular-ascii-conums.ttf differ diff --git a/docs/src/main/resources/theme/fonts/mplus1p-regular-fallback.ttf b/docs/src/main/resources/theme/fonts/mplus1p-regular-fallback.ttf new file mode 100644 index 0000000000000..5251e5c4b6d41 Binary files /dev/null and b/docs/src/main/resources/theme/fonts/mplus1p-regular-fallback.ttf differ diff --git a/docs/src/main/resources/theme/images/quarkus-logo.svg b/docs/src/main/resources/theme/images/quarkus-logo.svg new file mode 100644 index 0000000000000..39870a33ffe86 --- /dev/null +++ b/docs/src/main/resources/theme/images/quarkus-logo.svg @@ -0,0 +1 @@ +quarkus_logo_horizontal_rgb_1280px_default \ No newline at end of file diff --git a/docs/src/main/resources/theme/quarkus-theme.yml b/docs/src/main/resources/theme/quarkus-theme.yml new file mode 100644 index 0000000000000..4690832c651f2 --- /dev/null +++ b/docs/src/main/resources/theme/quarkus-theme.yml @@ -0,0 +1,314 @@ +# Hibernate theme based on the default theme +# Based on revision: https://github.com/asciidoctor/asciidoctor-pdf/blob/fa5c3e383aaaa0e6634a7ac9380401cc796893f1/data/themes/default-theme.yml +# The default-theme.yml used as a base is also present in this directory. + +font: + catalog: + Overpass: + normal: Overpass-Regular-conums.ttf + italic: Overpass-Italic.ttf + bold: Overpass-Bold.ttf + bold_italic: Overpass-BoldItalic.ttf + Overpass Mono: + normal: OverpassMono-Regular-conums.ttf + italic: OverpassMono-Regular-conums.ttf + bold: OverpassMono-Bold.ttf + bold_italic: OverpassMono-Bold.ttf + # M+ 1mn supports ASCII and the circled numbers used for conums + M+ 1p Fallback: + normal: mplus1p-regular-fallback.ttf + bold: mplus1p-regular-fallback.ttf + italic: mplus1p-regular-fallback.ttf + bold_italic: mplus1p-regular-fallback.ttf + fallbacks: + - M+ 1p Fallback +page: + background_color: FFFFFF + layout: portrait + initial_zoom: FitH + margin: [0.75in, 0.8in, 0.75in, 0.8in] + # margin_inner and margin_outer keys are used for recto/verso print margins when media=press + margin_inner: 1.0in + margin_outer: 0.75in + size: A4 +base: + align: justify + # color as hex string (leading # is optional) + font_color: 333333 + # color as RGB array + #font_color: [51, 51, 51] + # color as CMYK array (approximated) + #font_color: [0, 0, 0, 0.92] + #font_color: [0, 0, 0, 92%] + font_family: Overpass + # choose one of these font_size/line_height_length combinations + #font_size: 14 + #line_height_length: 20 + #font_size: 11.25 + #line_height_length: 18 + #font_size: 11.2 + #line_height_length: 16 + font_size: 10.5 + #line_height_length: 15 + # correct line height for Noto Serif metrics + line_height_length: 12 + #font_size: 11.25 + #line_height_length: 18 + line_height: $base_line_height_length / $base_font_size + font_size_large: round($base_font_size * 1.25) + font_size_small: round($base_font_size * 0.85) + font_size_min: $base_font_size * 0.75 + font_style: normal + border_color: EEEEEE + border_radius: 4 + border_width: 0.5 +role: + line-through: + text_decoration: line-through + underline: + text_decoration: underline + big: + font_size: $base_font_size_large + small: + font_size: $base_font_size_small + subtitle: + font_size: 0.8em + font_color: 999999 +# FIXME vertical_rhythm is weird; we should think in terms of ems +#vertical_rhythm: $base_line_height_length * 2 / 3 +# correct line height for Noto Serif metrics (comes with built-in line height) +vertical_rhythm: $base_line_height_length +horizontal_rhythm: $base_line_height_length +# QUESTION should vertical_spacing be block_spacing instead? +vertical_spacing: $vertical_rhythm +link: + font_color: 428BCA +# literal is currently used for inline monospaced in prose and table cells +literal: + font_color: B12146 + font_family: Overpass Mono +button: + content: "[\u2009%s\u2009]" + font_style: bold +key: + background_color: F5F5F5 + border_color: CCCCCC + border_offset: 2 + border_radius: 2 + border_width: 0.5 + font_family: $literal_font_family + separator: "\u202f+\u202f" +mark: + background_color: FFFF00 + border_offset: 1 +menu: + caret_content: " \u203a " +heading: + align: left + font_color: $base_font_color + font_style: bold + # h1 is used for part titles (book doctype) or the doctitle (article doctype) + h1_font_size: floor($base_font_size * 2.6) + # h2 is used for chapter titles (book doctype only) + h2_font_size: floor($base_font_size * 2.15) + h3_font_size: round($base_font_size * 1.7) + h4_font_size: $base_font_size_large + h5_font_size: $base_font_size + h6_font_size: $base_font_size_small + #line_height: 1.4 + # correct line height for Noto Serif metrics (comes with built-in line height) + line_height: 1 + margin_top: $vertical_rhythm * 0.4 + margin_bottom: $vertical_rhythm * 0.9 + min_height_after: $base_line_height_length * 1.5 +title_page: + align: right + logo: + top: 10% + title: + top: 55% + font_size: $heading_h1_font_size + font_color: 999999 + line_height: 0.9 + subtitle: + font_size: $heading_h3_font_size + font_style: bold_italic + line_height: 1 + authors: + margin_top: $base_font_size * 1.25 + font_size: $base_font_size_large + font_color: 181818 + revision: + margin_top: $base_font_size * 1.25 +block: + margin_top: 0 + margin_bottom: $vertical_rhythm +caption: + align: left + font_size: $base_font_size * 0.95 + font_style: italic + # FIXME perhaps set line_height instead of / in addition to margins? + margin_inside: $vertical_rhythm / 3 + #margin_inside: $vertical_rhythm / 4 + margin_outside: 0 +lead: + font_size: $base_font_size_large + line_height: 1.4 +abstract: + font_color: 5C6266 + font_size: $lead_font_size + line_height: $lead_line_height + font_style: italic + first_line_font_style: bold + title: + align: center + font_color: $heading_font_color + font_size: $heading_h4_font_size + font_style: $heading_font_style +admonition: + column_rule_color: $base_border_color + column_rule_width: $base_border_width + padding: [0, $horizontal_rhythm, 0, $horizontal_rhythm] + #icon: + # tip: + # name: far-lightbulb + # stroke_color: 111111 + # size: 24 + label: + text_transform: uppercase + font_style: bold +blockquote: + font_size: $base_font_size_large + border_color: $base_border_color + border_width: 0 + border_left_width: 5 + # FIXME disable negative padding bottom once margin collapsing is implemented + padding: [0, $horizontal_rhythm, $block_margin_bottom * -0.75, $horizontal_rhythm + $blockquote_border_left_width / 2] + cite_font_size: $base_font_size_small + cite_font_color: 999999 +verse: + font_size: $blockquote_font_size + border_color: $blockquote_border_color + border_width: $blockquote_border_width + border_left_width: $blockquote_border_left_width + padding: $blockquote_padding + cite_font_size: $blockquote_cite_font_size + cite_font_color: $blockquote_cite_font_color +# code is used for source blocks (perhaps change to source or listing?) +code: + font_color: $base_font_color + font_family: $literal_font_family + font_size: ceil($base_font_size) + padding: $code_font_size + line_height: 1.1 + # line_gap is an experimental property to control how a background color is applied to an inline block element + line_gap: 3.8 + background_color: F5F5F5 + border_color: CCCCCC + border_radius: $base_border_radius + border_width: 0.75 +conum: + font_family: $literal_font_family + font_color: $literal_font_color + font_size: $base_font_size + line_height: 4 / 3 + glyphs: circled +example: + border_color: $base_border_color + border_radius: $base_border_radius + border_width: 0.75 + background_color: FFFFFF + # FIXME reenable padding bottom once margin collapsing is implemented + padding: [$vertical_rhythm * 0.5, $horizontal_rhythm * 0.5, 0, $horizontal_rhythm * 0.5] +image: + align: left +prose: + margin_top: $block_margin_top + margin_bottom: $block_margin_bottom +sidebar: + background_color: EEEEEE + border_color: E1E1E1 + border_radius: $base_border_radius + border_width: $base_border_width + # FIXME reenable padding bottom once margin collapsing is implemented + padding: [$vertical_rhythm, $vertical_rhythm * 1.25, 0, $vertical_rhythm * 1.25] + title: + align: center + font_color: $heading_font_color + font_size: $heading_h4_font_size + font_style: $heading_font_style +thematic_break: + border_color: $base_border_color + border_style: solid + border_width: $base_border_width + margin_top: $vertical_rhythm * 0.5 + margin_bottom: $vertical_rhythm * 1.5 +description_list: + term_font_style: bold + term_spacing: $vertical_rhythm / 4 + description_indent: $horizontal_rhythm * 1.25 +outline_list: + indent: $horizontal_rhythm * 1.5 + #marker_font_color: 404040 + # NOTE outline_list_item_spacing applies to list items that do not have complex content + item_spacing: $vertical_rhythm / 2 +table: + background_color: $page_background_color + border_color: DDDDDD + border_width: $base_border_width + cell_padding: 3 + head: + font_style: bold + border_bottom_width: $base_border_width * 2.5 + body: + stripe_background_color: F9F9F9 + foot: + background_color: F0F0F0 +toc: + indent: $horizontal_rhythm + line_height: 1.4 + dot_leader: + #content: ". " + font_color: A9A9A9 + #levels: 2 3 +footnotes: + font_size: round($base_font_size * 0.75) + item_spacing: $outline_list_item_spacing / 2 +# Couldn't get the logo to have proper margin. +#header: +# font_size: $base_font_size_small +# line_height: 1 +# vertical_align: middle +# border_color: DDDDDD +# border_width: 0.25 +# height: $base_line_height_length * 2.5 +# padding: [$base_line_height_length / 2, 1, 0, 1] +# recto: +# columns: "<50% =0% >50%" +# left: +# content: 'image:images/quarkus-logo.svg[fit=contain,padding="10,0,10,0"]' +# right: +# content: '' +# verso: +# columns: $header_recto_columns +# left: +# content: $header_recto_left_content +# right: +# content: $header_recto_right_content +footer: + font_size: $base_font_size_small + # NOTE if background_color is set, background and border will span width of page + border_color: DDDDDD + border_width: 0.25 + height: $base_line_height_length * 2.5 + line_height: 1 + padding: [$base_line_height_length / 2, 1, 0, 1] + vertical_align: top + recto: + #columns: "<50% =0% >50%" + right: + content: '{page-number}' + verso: + #columns: $footer_recto_columns + left: + content: $footer_recto_right_content diff --git a/extensions/agroal/deployment/pom.xml b/extensions/agroal/deployment/pom.xml index 03167b34a1efd..7449ad4ad97e1 100644 --- a/extensions/agroal/deployment/pom.xml +++ b/extensions/agroal/deployment/pom.xml @@ -26,6 +26,10 @@ io.quarkus quarkus-agroal + + io.quarkus + quarkus-agroal-spi + io.quarkus quarkus-narayana-jta-deployment @@ -34,8 +38,22 @@ io.quarkus quarkus-smallrye-health-spi + + io.quarkus + quarkus-smallrye-metrics-spi + + + io.quarkus + quarkus-resteasy-deployment + test + + + io.rest-assured + rest-assured + test + io.quarkus quarkus-junit5-internal diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java index 3e74f95b06f67..f210b1d1f8438 100644 --- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java +++ b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java @@ -1,9 +1,11 @@ package io.quarkus.agroal.deployment; +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; import java.sql.Driver; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Map.Entry; import java.util.Optional; @@ -15,6 +17,10 @@ import javax.enterprise.inject.spi.DeploymentException; import javax.sql.XADataSource; +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.Tag; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; @@ -23,6 +29,8 @@ import io.agroal.api.AgroalDataSource; import io.quarkus.agroal.DataSource; +import io.quarkus.agroal.metrics.AgroalCounter; +import io.quarkus.agroal.metrics.AgroalGauge; import io.quarkus.agroal.runtime.AbstractDataSourceProducer; import io.quarkus.agroal.runtime.AgroalBuildTimeConfig; import io.quarkus.agroal.runtime.AgroalRecorder; @@ -34,6 +42,7 @@ import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.processor.DotNames; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -51,6 +60,7 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; +import io.quarkus.smallrye.metrics.deployment.spi.MetricBuildItem; class AgroalProcessor { @@ -76,7 +86,8 @@ BeanContainerListenerBuildItem build( BuildProducer resource, BuildProducer dataSourceDriver, SslNativeConfigBuildItem sslNativeConfig, BuildProducer sslNativeSupport, - BuildProducer generatedBean) throws Exception { + BuildProducer generatedBean, + Capabilities capabilities) throws Exception { feature.produce(new FeatureBuildItem(FeatureBuildItem.AGROAL)); @@ -127,7 +138,8 @@ BeanContainerListenerBuildItem build( String dataSourceProducerClassName = AbstractDataSourceProducer.class.getPackage().getName() + "." + "DataSourceProducer"; - createDataSourceProducerBean(generatedBean, dataSourceProducerClassName); + createDataSourceProducerBean(generatedBean, dataSourceProducerClassName, + capabilities.isCapabilityPresent(Capabilities.METRICS)); return new BeanContainerListenerBuildItem(recorder.addDataSource( (Class) recorderContext.classProxy(dataSourceProducerClassName), @@ -190,14 +202,22 @@ private static void validateBuildTimeConfig(final String datasourceName, final D void configureRuntimeProperties(AgroalRecorder recorder, BuildProducer dataSourceInitialized, AgroalRuntimeConfig agroalRuntimeConfig) { - if (!agroalBuildTimeConfig.defaultDataSource.driver.isPresent() && agroalBuildTimeConfig.namedDataSources.isEmpty()) { + Optional defaultDataSourceDriver = agroalBuildTimeConfig.defaultDataSource.driver; + if (!defaultDataSourceDriver.isPresent() && agroalBuildTimeConfig.namedDataSources.isEmpty()) { // No datasource has been configured so bail out return; } recorder.configureRuntimeProperties(agroalRuntimeConfig); - dataSourceInitialized.produce(new DataSourceInitializedBuildItem()); + Set dataSourceNames = agroalBuildTimeConfig.namedDataSources.keySet(); + DataSourceInitializedBuildItem buildItem; + if (defaultDataSourceDriver.isPresent()) { + buildItem = DataSourceInitializedBuildItem.ofDefaultDataSourceAnd(dataSourceNames); + } else { + buildItem = DataSourceInitializedBuildItem.ofDataSources(dataSourceNames); + } + dataSourceInitialized.produce(buildItem); } @BuildStep @@ -220,7 +240,7 @@ UnremovableBeanBuildItem markBeansAsUnremovable() { * Build time and runtime configuration are both injected into this bean. */ private void createDataSourceProducerBean(BuildProducer generatedBean, - String dataSourceProducerClassName) { + String dataSourceProducerClassName, boolean metricsCapabilityPresent) { ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(generatedBean); ClassCreator classCreator = ClassCreator.builder().classOutput(classOutput) @@ -244,15 +264,16 @@ private void createDataSourceProducerBean(BuildProducer ResultHandle dataSourceRuntimeConfig = defaultDataSourceMethodCreator.invokeVirtualMethod( MethodDescriptor.ofMethod(AbstractDataSourceProducer.class, "getDefaultRuntimeConfig", Optional.class), defaultDataSourceMethodCreator.getThis()); + ResultHandle mpMetricsEnabled = defaultDataSourceMethodCreator.load(metricsCapabilityPresent); defaultDataSourceMethodCreator.returnValue( defaultDataSourceMethodCreator.invokeVirtualMethod( MethodDescriptor.ofMethod(AbstractDataSourceProducer.class, "createDataSource", AgroalDataSource.class, String.class, - DataSourceBuildTimeConfig.class, Optional.class), + DataSourceBuildTimeConfig.class, Optional.class, boolean.class), defaultDataSourceMethodCreator.getThis(), dataSourceName, - dataSourceBuildTimeConfig, dataSourceRuntimeConfig)); + dataSourceBuildTimeConfig, dataSourceRuntimeConfig, mpMetricsEnabled)); } for (Entry namedDataSourceEntry : agroalBuildTimeConfig.namedDataSources @@ -284,15 +305,16 @@ private void createDataSourceProducerBean(BuildProducer MethodDescriptor.ofMethod(AbstractDataSourceProducer.class, "getRuntimeConfig", Optional.class, String.class), namedDataSourceMethodCreator.getThis(), namedDataSourceNameRH); + ResultHandle mpMetricsEnabled = namedDataSourceMethodCreator.load(metricsCapabilityPresent); namedDataSourceMethodCreator.returnValue( namedDataSourceMethodCreator.invokeVirtualMethod( MethodDescriptor.ofMethod(AbstractDataSourceProducer.class, "createDataSource", AgroalDataSource.class, String.class, - DataSourceBuildTimeConfig.class, Optional.class), + DataSourceBuildTimeConfig.class, Optional.class, boolean.class), namedDataSourceMethodCreator.getThis(), namedDataSourceNameRH, - namedDataSourceBuildTimeConfig, namedDataSourceRuntimeConfig)); + namedDataSourceBuildTimeConfig, namedDataSourceRuntimeConfig, mpMetricsEnabled)); } classCreator.close(); @@ -303,4 +325,200 @@ HealthBuildItem addHealthCheck(AgroalBuildTimeConfig agroalBuildTimeConfig) { return new HealthBuildItem("io.quarkus.agroal.runtime.health.DataSourceHealthCheck", agroalBuildTimeConfig.healthEnabled, "datasource"); } + + @BuildStep + void registerMetrics(AgroalBuildTimeConfig agroalBuildTimeConfig, + BuildProducer metrics) { + Metadata activeCountMetadata = Metadata.builder() + .withName("agroal.active.count") + .withDescription("Number of active connections. These connections are in use and not available to be acquired.") + .withType(MetricType.GAUGE) + .build(); + Metadata availableCountMetadata = Metadata.builder() + .withName("agroal.available.count") + .withDescription("Number of idle connections in the pool, available to be acquired.") + .withType(MetricType.GAUGE) + .build(); + Metadata maxUsedCountMetadata = Metadata.builder() + .withName("agroal.max.used.count") + .withDescription("Maximum number of connections active simultaneously.") + .withType(MetricType.GAUGE) + .build(); + Metadata awaitingCountMetadata = Metadata.builder() + .withName("agroal.awaiting.count") + .withDescription("Approximate number of threads blocked, waiting to acquire a connection.") + .withType(MetricType.GAUGE) + .build(); + Metadata blockingTimeAverageMetadata = Metadata.builder() + .withName("agroal.blocking.time.average") + .withDescription("Average time an application waited to acquire a connection.") + .withUnit(MetricUnits.MILLISECONDS) + .withType(MetricType.GAUGE) + .build(); + Metadata blockingTimeMaxMetadata = Metadata.builder() + .withName("agroal.blocking.time.max") + .withDescription("Maximum time an application waited to acquire a connection.") + .withUnit(MetricUnits.MILLISECONDS) + .withType(MetricType.GAUGE) + .build(); + Metadata blockingTimeTotalMetadata = Metadata.builder() + .withName("agroal.blocking.time.total") + .withDescription("Total time applications waited to acquire a connection.") + .withUnit(MetricUnits.MILLISECONDS) + .withType(MetricType.GAUGE) + .build(); + Metadata creationTimeAverageMetadata = Metadata.builder() + .withName("agroal.creation.time.average") + .withDescription("Average time for a connection to be created.") + .withUnit(MetricUnits.MILLISECONDS) + .withType(MetricType.GAUGE) + .build(); + Metadata creationTimeMaxMetadata = Metadata.builder() + .withName("agroal.creation.time.max") + .withDescription("Maximum time for a connection to be created.") + .withUnit(MetricUnits.MILLISECONDS) + .withType(MetricType.GAUGE) + .build(); + Metadata creationTimeTotalMetadata = Metadata.builder() + .withName("agroal.creation.time.total") + .withDescription("Total time waiting for connections to be created.") + .withUnit(MetricUnits.MILLISECONDS) + .withType(MetricType.GAUGE) + .build(); + Metadata acquireCountMetadata = Metadata.builder() + .withName("agroal.acquire.count") + .withDescription("Number of times an acquire operation succeeded.") + .withType(MetricType.COUNTER) + .build(); + Metadata creationCountMetadata = Metadata.builder() + .withName("agroal.creation.count") + .withDescription("Number of created connections.") + .withType(MetricType.COUNTER) + .build(); + Metadata leakDetectionCountMetadata = Metadata.builder() + .withName("agroal.leak.detection.count") + .withDescription("Number of times a leak was detected. A single connection can be detected multiple times.") + .withType(MetricType.COUNTER) + .build(); + Metadata destroyCountMetadata = Metadata.builder() + .withName("agroal.destroy.count") + .withDescription("Number of destroyed connections.") + .withType(MetricType.COUNTER) + .build(); + Metadata flushCountMetadata = Metadata.builder() + .withName("agroal.flush.count") + .withDescription("Number of connections removed from the pool, not counting invalid / idle.") + .withType(MetricType.COUNTER) + .build(); + Metadata invalidCountMetadata = Metadata.builder() + .withName("agroal.invalid.count") + .withDescription("Number of connections removed from the pool for being invalid.") + .withType(MetricType.COUNTER) + .build(); + Metadata reapCountMetadata = Metadata.builder() + .withName("agroal.reap.count") + .withDescription("Number of connections removed from the pool for being idle.") + .withType(MetricType.COUNTER) + .build(); + + HashMap datasources = new HashMap<>(agroalBuildTimeConfig.namedDataSources); + if (agroalBuildTimeConfig.defaultDataSource != null) { + datasources.put(null, agroalBuildTimeConfig.defaultDataSource); + } + + for (Entry dataSourceEntry : datasources.entrySet()) { + String dataSourceName = dataSourceEntry.getKey(); + // expose metrics for this datasource if metrics are enabled both globally and for this data source + // (they are enabled for each data source by default if they are also enabled globally) + boolean metricsEnabledForThisDatasource = agroalBuildTimeConfig.metricsEnabled && + dataSourceEntry.getValue().enableMetrics.orElse(true); + Tag tag = new Tag("datasource", dataSourceName != null ? dataSourceName : "default"); + String configRootName = "datasource"; + metrics.produce(new MetricBuildItem(activeCountMetadata, + new AgroalGauge(dataSourceName, "activeCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(maxUsedCountMetadata, + new AgroalGauge(dataSourceName, "maxUsedCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(awaitingCountMetadata, + new AgroalGauge(dataSourceName, "awaitingCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(availableCountMetadata, + new AgroalGauge(dataSourceName, "availableCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(blockingTimeAverageMetadata, + new AgroalGauge(dataSourceName, "blockingTimeAverage"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(blockingTimeMaxMetadata, + new AgroalGauge(dataSourceName, "blockingTimeMax"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(blockingTimeTotalMetadata, + new AgroalGauge(dataSourceName, "blockingTimeTotal"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(creationTimeAverageMetadata, + new AgroalGauge(dataSourceName, "creationTimeAverage"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(creationTimeMaxMetadata, + new AgroalGauge(dataSourceName, "creationTimeMax"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(creationTimeTotalMetadata, + new AgroalGauge(dataSourceName, "creationTimeTotal"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(acquireCountMetadata, + new AgroalCounter(dataSourceName, "acquireCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(creationCountMetadata, + new AgroalCounter(dataSourceName, "creationCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(leakDetectionCountMetadata, + new AgroalCounter(dataSourceName, "leakDetectionCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(destroyCountMetadata, + new AgroalCounter(dataSourceName, "destroyCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(flushCountMetadata, + new AgroalCounter(dataSourceName, "flushCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(invalidCountMetadata, + new AgroalCounter(dataSourceName, "invalidCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + metrics.produce(new MetricBuildItem(reapCountMetadata, + new AgroalCounter(dataSourceName, "reapCount"), + metricsEnabledForThisDatasource, + configRootName, + tag)); + } + } } diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java deleted file mode 100644 index 0a5329d3f437c..0000000000000 --- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.quarkus.agroal.deployment; - -import io.quarkus.builder.item.SimpleBuildItem; - -/** - * Marker build item indicating the datasource has been fully initialized. - */ -public final class DataSourceInitializedBuildItem extends SimpleBuildItem { - - public DataSourceInitializedBuildItem() { - } -} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalDevModeTestCase.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalDevModeTestCase.java new file mode 100644 index 0000000000000..56c4352b29548 --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalDevModeTestCase.java @@ -0,0 +1,43 @@ +package io.quarkus.agroal.test; + +import java.util.function.Supplier; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class AgroalDevModeTestCase { + + @RegisterExtension + public static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .setArchiveProducer(new Supplier() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClass(DevModeResource.class) + .add(new StringAsset("quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:testing\n" + + "quarkus.datasource.driver=org.h2.Driver\n" + + "quarkus.datasource.username=USERNAME-NAMED\n"), "application.properties"); + } + }); + + @Test + public void testAgroalHotReplacement() { + RestAssured + .get("/dev/user") + .then() + .body(Matchers.equalTo("USERNAME-NAMED")); + test.modifyResourceFile("application.properties", s -> s.replace("USERNAME-NAMED", "OTHER-USER")); + + RestAssured + .get("/dev/user") + .then() + .body(Matchers.equalTo("OTHER-USER")); + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DevModeResource.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DevModeResource.java new file mode 100644 index 0000000000000..f9aa6c7f1ba61 --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DevModeResource.java @@ -0,0 +1,31 @@ +package io.quarkus.agroal.test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.inject.Inject; +import javax.sql.DataSource; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/dev") +public class DevModeResource { + + @Inject + DataSource dataSource; + + @GET + @Path("/user") + public String getUser() throws SQLException { + try (Connection c = dataSource.getConnection()) { + try (Statement s = c.createStatement()) { + try (ResultSet rs = s.executeQuery("SELECT USER()")) { + rs.next(); + return rs.getString(1); + } + } + } + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/GcpDataSourceConfigTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/GcpDataSourceConfigTest.java new file mode 100644 index 0000000000000..b7ed6ebbae26c --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/GcpDataSourceConfigTest.java @@ -0,0 +1,63 @@ +package io.quarkus.agroal.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.SQLException; +import java.time.Duration; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration; +import io.agroal.api.configuration.AgroalConnectionPoolConfiguration; +import io.agroal.narayana.NarayanaTransactionIntegration; +import io.quarkus.test.QuarkusUnitTest; + +public class GcpDataSourceConfigTest { + + //tag::injection[] + @Inject + AgroalDataSource defaultDataSource; + //end::injection[] + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-gcp-datasource.properties"); + + @Test + public void testGCPDataSourceInjection() throws SQLException { + testDataSource(defaultDataSource, "username-default", 3, 13, 7, Duration.ofSeconds(53), Duration.ofSeconds(54), + Duration.ofSeconds(55), Duration.ofSeconds(56), Duration.ofSeconds(57), + "create schema if not exists schema_default"); + } + + private static void testDataSource(AgroalDataSource dataSource, String username, int minSize, int maxSize, + int initialSize, Duration backgroundValidationInterval, Duration acquisitionTimeout, Duration leakDetectionInterval, + Duration idleRemovalInterval, Duration maxLifetime, String newConnectionSql) throws SQLException { + AgroalConnectionPoolConfiguration configuration = dataSource.getConfiguration().connectionPoolConfiguration(); + AgroalConnectionFactoryConfiguration agroalConnectionFactoryConfiguration = configuration + .connectionFactoryConfiguration(); + assertEquals( + "jdbc:h2:tcp://localhost/mem:default?socketFactory=com.google.cloud.sql.postgres.SocketFactory&cloudSqlInstance=project:zone:db-name", + agroalConnectionFactoryConfiguration.jdbcUrl()); + assertEquals(username, agroalConnectionFactoryConfiguration.principal().getName()); + assertEquals(minSize, configuration.minSize()); + assertEquals(maxSize, configuration.maxSize()); + assertEquals(initialSize, configuration.initialSize()); + assertEquals(backgroundValidationInterval, configuration.validationTimeout()); + assertEquals(acquisitionTimeout, configuration.acquisitionTimeout()); + assertEquals(leakDetectionInterval, configuration.leakTimeout()); + assertEquals(idleRemovalInterval, configuration.reapTimeout()); + assertEquals(maxLifetime, configuration.maxLifetime()); + assertTrue(configuration.transactionIntegration() instanceof NarayanaTransactionIntegration); + assertEquals(AgroalConnectionFactoryConfiguration.TransactionIsolation.SERIALIZABLE, + agroalConnectionFactoryConfiguration.jdbcTransactionIsolation()); + assertTrue(agroalConnectionFactoryConfiguration.trackJdbcResources()); + assertTrue(dataSource.getConfiguration().metricsEnabled()); + assertEquals(newConnectionSql, agroalConnectionFactoryConfiguration.initialSql()); + } +} diff --git a/extensions/agroal/deployment/src/test/resources/application-gcp-datasource.properties b/extensions/agroal/deployment/src/test/resources/application-gcp-datasource.properties new file mode 100644 index 0000000000000..9ed2a363cea78 --- /dev/null +++ b/extensions/agroal/deployment/src/test/resources/application-gcp-datasource.properties @@ -0,0 +1,18 @@ +#tag::basic[] +quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:default +quarkus.datasource.driver=org.h2.Driver +quarkus.datasource.username=username-default +quarkus.datasource.min-size=3 +quarkus.datasource.max-size=13 +quarkus.datasource.enable-metrics=true +#end::basic[] +quarkus.datasource.initial-size=7 +quarkus.datasource.background-validation-interval=53 +quarkus.datasource.acquisition-timeout=54 +quarkus.datasource.leak-detection-interval=55 +quarkus.datasource.idle-removal-interval=56 +quarkus.datasource.max-lifetime=57 +quarkus.datasource.transaction-isolation-level=serializable +quarkus.datasource.new-connection-sql=create schema if not exists schema_default +quarkus.datasource.use-gcp=true +quarkus.datasource.cloud-sql-instance=project:zone:db-name \ No newline at end of file diff --git a/extensions/agroal/pom.xml b/extensions/agroal/pom.xml index 47b4f6984e30d..4eb57e49b3eb2 100644 --- a/extensions/agroal/pom.xml +++ b/extensions/agroal/pom.xml @@ -14,6 +14,7 @@ Quarkus - Agroal pom + spi deployment runtime diff --git a/extensions/agroal/runtime/pom.xml b/extensions/agroal/runtime/pom.xml index 3600282874ecd..8ac3373d62ce9 100644 --- a/extensions/agroal/runtime/pom.xml +++ b/extensions/agroal/runtime/pom.xml @@ -37,6 +37,11 @@ quarkus-smallrye-health true + + io.quarkus + quarkus-smallrye-metrics + true + org.jboss.narayana.jta diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/AgroalCounter.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/AgroalCounter.java new file mode 100644 index 0000000000000..92a2724c4133c --- /dev/null +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/AgroalCounter.java @@ -0,0 +1,98 @@ +package io.quarkus.agroal.metrics; + +import org.eclipse.microprofile.metrics.Counter; + +import io.agroal.api.AgroalDataSource; +import io.agroal.api.AgroalDataSourceMetrics; +import io.quarkus.arc.Arc; + +public class AgroalCounter implements Counter { + + private String dataSourceName; + private volatile AgroalDataSource dataSource; + private String metric; + + public AgroalCounter() { + + } + + /** + * @param dataSourceName Which datasource should be queried for metric + * @param metricName Name of the method from DataSource.getMetrics() that should be called to retrieve the particular value. + * This has nothing to do with the metric name from MP Metrics point of view! + */ + public AgroalCounter(String dataSourceName, String metricName) { + this.dataSourceName = dataSourceName; + this.metric = metricName; + } + + public String getDataSourceName() { + return dataSourceName; + } + + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + private AgroalDataSource getDataSource() { + AgroalDataSource dsLocal = dataSource; + if (dsLocal == null) { + synchronized (this) { + dsLocal = dataSource; + if (dsLocal == null) { + if (dataSourceName == null) { + dataSource = dsLocal = Arc.container().instance(AgroalDataSource.class).get(); + } else { + dataSource = dsLocal = Arc.container() + .instance(AgroalDataSource.class, new DataSourceLiteral(dataSourceName)) + .get(); + } + } + } + } + return dsLocal; + } + + public void setDataSource(AgroalDataSource dataSource) { + this.dataSource = dataSource; + } + + public String getMetric() { + return metric; + } + + public void setMetric(String metric) { + this.metric = metric; + } + + @Override + public void inc() { + } + + @Override + public void inc(long n) { + } + + @Override + public long getCount() { + AgroalDataSourceMetrics metrics = getDataSource().getMetrics(); + switch (metric) { + case "acquireCount": + return metrics.acquireCount(); + case "creationCount": + return metrics.creationCount(); + case "leakDetectionCount": + return metrics.leakDetectionCount(); + case "destroyCount": + return metrics.destroyCount(); + case "flushCount": + return metrics.flushCount(); + case "invalidCount": + return metrics.invalidCount(); + case "reapCount": + return metrics.reapCount(); + default: + throw new IllegalArgumentException("Unknown datasource metric"); + } + } +} diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/AgroalGauge.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/AgroalGauge.java new file mode 100644 index 0000000000000..20089fd222a4c --- /dev/null +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/AgroalGauge.java @@ -0,0 +1,92 @@ +package io.quarkus.agroal.metrics; + +import org.eclipse.microprofile.metrics.Gauge; + +import io.agroal.api.AgroalDataSource; +import io.agroal.api.AgroalDataSourceMetrics; +import io.quarkus.arc.Arc; + +public class AgroalGauge implements Gauge { + + private String dataSourceName; + private volatile AgroalDataSource dataSource; + private String metric; + + public AgroalGauge() { + + } + + /** + * @param dataSourceName Which datasource should be queried for metric + * @param metricName Name of the method from DataSource.getMetrics() that should be called to retrieve the particular value. + * This has nothing to do with the metric name from MP Metrics point of view! + */ + public AgroalGauge(String dataSourceName, String metricName) { + this.dataSourceName = dataSourceName; + this.metric = metricName; + } + + public String getDataSourceName() { + return dataSourceName; + } + + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + public String getMetric() { + return metric; + } + + public void setMetric(String metric) { + this.metric = metric; + } + + private AgroalDataSource getDataSource() { + AgroalDataSource dsLocal = dataSource; + if (dsLocal == null) { + synchronized (this) { + dsLocal = dataSource; + if (dsLocal == null) { + if (dataSourceName == null) { + dataSource = dsLocal = Arc.container().instance(AgroalDataSource.class).get(); + } else { + dataSource = dsLocal = Arc.container() + .instance(AgroalDataSource.class, new DataSourceLiteral(dataSourceName)) + .get(); + } + } + } + } + return dsLocal; + } + + @Override + public Long getValue() { + AgroalDataSourceMetrics metrics = getDataSource().getMetrics(); + switch (metric) { + case "activeCount": + return metrics.activeCount(); + case "availableCount": + return metrics.availableCount(); + case "maxUsedCount": + return metrics.maxUsedCount(); + case "awaitingCount": + return metrics.awaitingCount(); + case "blockingTimeAverage": + return metrics.blockingTimeAverage().toMillis(); + case "blockingTimeMax": + return metrics.blockingTimeMax().toMillis(); + case "blockingTimeTotal": + return metrics.blockingTimeTotal().toMillis(); + case "creationTimeAverage": + return metrics.creationTimeAverage().toMillis(); + case "creationTimeMax": + return metrics.creationTimeMax().toMillis(); + case "creationTimeTotal": + return metrics.creationTimeTotal().toMillis(); + default: + throw new IllegalArgumentException("Unknown data source metric"); + } + } +} diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/DataSourceLiteral.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/DataSourceLiteral.java new file mode 100644 index 0000000000000..3c4e60c800782 --- /dev/null +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/metrics/DataSourceLiteral.java @@ -0,0 +1,20 @@ +package io.quarkus.agroal.metrics; + +import javax.enterprise.util.AnnotationLiteral; + +import io.quarkus.agroal.DataSource; + +public class DataSourceLiteral extends AnnotationLiteral implements DataSource { + + private String name; + + public DataSourceLiteral(String name) { + this.name = name; + } + + @Override + public String value() { + return name; + } + +} diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AbstractDataSourceProducer.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AbstractDataSourceProducer.java index f084f7937479a..492bbd529f2c8 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AbstractDataSourceProducer.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AbstractDataSourceProducer.java @@ -3,6 +3,7 @@ import java.sql.Connection; import java.sql.Driver; import java.sql.Statement; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -67,7 +68,8 @@ public Optional getRuntimeConfig(String dataSourceName) public AgroalDataSource createDataSource(String dataSourceName, DataSourceBuildTimeConfig dataSourceBuildTimeConfig, - Optional dataSourceRuntimeConfigOptional) { + Optional dataSourceRuntimeConfigOptional, + boolean mpMetricsPresent) { if (!dataSourceRuntimeConfigOptional.isPresent() || !dataSourceRuntimeConfigOptional.get().url.isPresent()) { log.warn("Datasource " + dataSourceName + " not started: driver and/or url are not defined."); return null; @@ -125,7 +127,12 @@ public AgroalDataSource createDataSource(String dataSourceName, } // metrics - dataSourceConfiguration.metricsEnabled(dataSourceRuntimeConfig.enableMetrics); + if (dataSourceBuildTimeConfig.enableMetrics.isPresent()) { + dataSourceConfiguration.metricsEnabled(dataSourceBuildTimeConfig.enableMetrics.get()); + } else { + // if the enable-metrics property is unspecified, treat it as true if MP Metrics are being exposed + dataSourceConfiguration.metricsEnabled(buildTimeConfig.metricsEnabled && mpMetricsPresent); + } // Authentication if (dataSourceRuntimeConfig.username.isPresent()) { @@ -195,6 +202,20 @@ public boolean isValid(Connection connection) { poolConfiguration.maxLifetime(dataSourceRuntimeConfig.maxLifetime.get()); } + // CloudSQL config + if (dataSourceRuntimeConfig.useGcp) { + disableSslSupport(); + String cloudSqlInstance = dataSourceRuntimeConfig.cloudSqlInstance.orElseThrow( + () -> new RuntimeException("Cloud sql instance property is mandatory to use Gcp Cloudsql")); + if (cloudSqlInstance.split(":").length != 3) { + throw new RuntimeException("Cloud sql instance should match the pattern project-id:zone:cloudSqlInstance"); + } + agroalConnectionFactoryConfigurationSupplier.jdbcUrl( + MessageFormat.format("{0}?socketFactory=com.google.cloud.sql.postgres.SocketFactory&cloudSqlInstance={1}", + url, cloudSqlInstance)); + + } + // SSL support: we should push the driver specific code to the driver extensions but it will have to do for now if (disableSslSupport) { switch (driverName) { diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalBuildTimeConfig.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalBuildTimeConfig.java index 505fe89777675..816af56fa2c5a 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalBuildTimeConfig.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalBuildTimeConfig.java @@ -26,8 +26,17 @@ public class AgroalBuildTimeConfig { public Map namedDataSources; /** - * Whether or not an healtcheck is published in case the smallrye-health extension is present (default to true). + * Whether or not an health check is published in case the smallrye-health extension is present */ @ConfigItem(name = "health.enabled", defaultValue = "true") public boolean healthEnabled; + + /** + * Whether or not datasource metrics are published in case the smallrye-metrics extension is present (default to false). + * NOTE: This is different from the "enable-metrics" property that needs to be set on data source level to enable + * collection of metrics for that data source. + */ + @ConfigItem(name = "metrics.enabled", defaultValue = "false") + public boolean metricsEnabled; + } diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java index 7d12466b86bff..1c223cfd8586a 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java @@ -29,11 +29,6 @@ public void created(BeanContainer beanContainer) { } public void configureRuntimeProperties(AgroalRuntimeConfig agroalRuntimeConfig) { - // TODO @dmlloyd - // Same here, the map is entirely empty (obviously, I didn't expect the values - // that were not properly injected but at least the config objects present in - // the map) - // The elements from the default datasource are there Arc.container().instance(AbstractDataSourceProducer.class).get().setRuntimeConfig(agroalRuntimeConfig); } } diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceBuildTimeConfig.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceBuildTimeConfig.java index b66bdba4b8acd..424d234d211b7 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceBuildTimeConfig.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceBuildTimeConfig.java @@ -22,4 +22,11 @@ public class DataSourceBuildTimeConfig { @ConfigItem(defaultValue = "enabled") public TransactionIntegration transactions; + /** + * Enable datasource metrics collection. If unspecified, collecting metrics will be enabled by default if the + * smallrye-metrics extension is active. + */ + @ConfigItem + public Optional enableMetrics; + } diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceRuntimeConfig.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceRuntimeConfig.java index ca46829b31454..b893f6d49e24e 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceRuntimeConfig.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceRuntimeConfig.java @@ -103,12 +103,6 @@ public class DataSourceRuntimeConfig { @ConfigItem public Optional transactionIsolationLevel; - /** - * Enable datasource metrics collection. - */ - @ConfigItem - public boolean enableMetrics; - /** * When enabled Agroal will be able to produce a warning when a connection is returned * to the pool without the application having closed all open statements. @@ -130,4 +124,16 @@ public class DataSourceRuntimeConfig { */ @ConfigItem public Optional validationQuerySql; + + /** + * When enabled jdbc url will be forged to enable google socket factory + */ + @ConfigItem(defaultValue = "false") + public boolean useGcp; + + /** + * The name of the cloud sql instance + */ + @ConfigItem + public Optional cloudSqlInstance; } diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java index be14cd5829267..fc091785d7098 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java @@ -41,7 +41,7 @@ protected void init() { @Override public HealthCheckResponse call() { - HealthCheckResponseBuilder builder = HealthCheckResponse.named("Database connection(s) health check").up(); + HealthCheckResponseBuilder builder = HealthCheckResponse.named("Database connections health check").up(); for (Map.Entry dataSource : dataSources.entrySet()) { boolean isDefault = DEFAULT_DS.equals(dataSource.getKey()); try (Connection con = dataSource.getValue().getConnection()) { @@ -49,13 +49,13 @@ public HealthCheckResponse call() { if (!valid) { String data = isDefault ? "validation check failed for the default DataSource" : "validation check failed for DataSource '" + dataSource.getKey() + "'"; - String dsName = isDefault ? "quarkus-default-ds" : dataSource.getKey(); + String dsName = isDefault ? "default" : dataSource.getKey(); builder.down().withData(dsName, data); } } catch (SQLException e) { String data = isDefault ? "Unable to execute the validation check for the default DataSource: " : "Unable to execute the validation check for DataSource '" + dataSource.getKey() + "': "; - String dsName = isDefault ? "quarkus-default-ds" : dataSource.getKey(); + String dsName = isDefault ? "default" : dataSource.getKey(); builder.down().withData(dsName, data + e.getMessage()); } } diff --git a/extensions/agroal/spi/pom.xml b/extensions/agroal/spi/pom.xml new file mode 100644 index 0000000000000..b457ade0ddc41 --- /dev/null +++ b/extensions/agroal/spi/pom.xml @@ -0,0 +1,29 @@ + + + + quarkus-agroal-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-agroal-spi + Quarkus - Agroal - SPI + + + + io.quarkus + quarkus-core-deployment + + + + io.quarkus + quarkus-junit5-internal + test + + + + diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java similarity index 100% rename from extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java rename to extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java diff --git a/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java new file mode 100644 index 0000000000000..92472e7d39a97 --- /dev/null +++ b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java @@ -0,0 +1,67 @@ +package io.quarkus.agroal.deployment; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Marker build item indicating the datasource has been fully initialized. + *

+ * Contains all processed datasource names, including an empty string for the default datasource. + */ +public final class DataSourceInitializedBuildItem extends SimpleBuildItem { + private static final String DEFAULT_DATASOURCE_NAME = ""; + + private final String defaultDataSourceName; + private final Collection dataSourceNames = new ArrayList<>(); + + /** + * Null-safe way to get the datasource names of the given {@link DataSourceInitializedBuildItem}. + */ + public static final Collection dataSourceNamesOf(DataSourceInitializedBuildItem buildItem) { + return (buildItem != null) ? buildItem.getDataSourceNames() : Collections.emptyList(); + } + + /** + * Null-safe way to find out, if the given {@link DataSourceInitializedBuildItem} contains the default datasource. + */ + public static final boolean isDefaultDataSourcePresent(DataSourceInitializedBuildItem buildItem) { + return (buildItem != null) ? buildItem.isDefaultDataSourcePresent() : false; + } + + /** + * Creates a new instance of the default DataSource name and the given DataSource names. + */ + public static final DataSourceInitializedBuildItem ofDefaultDataSourceAnd(Collection dataSourceNames) { + return new DataSourceInitializedBuildItem(dataSourceNames, DEFAULT_DATASOURCE_NAME); + } + + /** + * Creates a new instance of the given DataSource names. + */ + public static final DataSourceInitializedBuildItem ofDataSources(Collection dataSourceNames) { + Collection allDataSourceNames = new ArrayList<>(dataSourceNames); + return new DataSourceInitializedBuildItem(allDataSourceNames, null); + } + + DataSourceInitializedBuildItem(Collection dataSourceNames, String defaultDataSourceName) { + this.dataSourceNames.addAll(dataSourceNames); + this.defaultDataSourceName = defaultDataSourceName; + } + + public Collection getDataSourceNames() { + return new ArrayList<>(dataSourceNames); + } + + public boolean isDefaultDataSourcePresent() { + return (defaultDataSourceName != null); + } + + @Override + public String toString() { + return "DataSourceInitializedBuildItem [defaultDataSourceName=" + defaultDataSourceName + ", dataSourceNames=" + + dataSourceNames + "]"; + } +} diff --git a/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceSchemaReadyBuildItem.java b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceSchemaReadyBuildItem.java new file mode 100644 index 0000000000000..a90b1ce43e930 --- /dev/null +++ b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceSchemaReadyBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.agroal.deployment; + +import java.util.Collection; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * A build item which can be used to order build processors which need a datasource's + * schema to be ready (which really means that the tables have been created and + * any migration run on them) for processing + */ +public final class DataSourceSchemaReadyBuildItem extends SimpleBuildItem { + + private final Collection datasourceNames; + + public DataSourceSchemaReadyBuildItem(final Collection datasourceNames) { + this.datasourceNames = datasourceNames; + } + + public Collection getDatasourceNames() { + return this.datasourceNames; + } +} diff --git a/extensions/agroal/spi/src/test/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItemTest.java b/extensions/agroal/spi/src/test/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItemTest.java new file mode 100644 index 0000000000000..9063ef7121c87 --- /dev/null +++ b/extensions/agroal/spi/src/test/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItemTest.java @@ -0,0 +1,109 @@ +package io.quarkus.agroal.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DataSourceInitializedBuildItemTest { + + private static final String DEFAULT_DATASOURCE = ""; + private static final String NO_DEFAULT_DATASOURCE = null; + + private DataSourceInitializedBuildItem buildItem; + + @Test + @DisplayName("datasource names are contained correctly") + void testDataSourceNamesContained() { + Collection expectedNames = new ArrayList<>(Arrays.asList("one", "another")); + buildItem = new DataSourceInitializedBuildItem(expectedNames, DEFAULT_DATASOURCE); + assertEquals(expectedNames, buildItem.getDataSourceNames()); + } + + @Test + @DisplayName("datasource names may not be changed from the outside using the given Collection") + void testDataSourceNamesNotChangeableByGivenCollection() { + Collection expectedNames = new ArrayList<>(Arrays.asList("", "one", "another")); + Collection givenCollection = new ArrayList<>(expectedNames); + buildItem = new DataSourceInitializedBuildItem(givenCollection, DEFAULT_DATASOURCE); + givenCollection.add("shouldBeIgnored"); + assertEquals(expectedNames, buildItem.getDataSourceNames()); + } + + @Test + @DisplayName("datasource names may not be changed from the outside using the returned Collection") + void testDataSourceNamesNotChangeableByReturnedCollection() { + Collection expectedNames = new ArrayList<>(Arrays.asList("one", "another")); + buildItem = new DataSourceInitializedBuildItem(expectedNames, DEFAULT_DATASOURCE); + assertNotSame(expectedNames, buildItem.getDataSourceNames()); + buildItem.getDataSourceNames().add("shouldBeIgnored"); + assertEquals(expectedNames, buildItem.getDataSourceNames()); + } + + @Test + @DisplayName("dataSourceNamesOf returns names correctly") + void testDataSourceNamesOfInstanceReturnsNames() { + Collection expectedNames = new ArrayList<>(Arrays.asList("one", "another")); + buildItem = new DataSourceInitializedBuildItem(expectedNames, DEFAULT_DATASOURCE); + assertEquals(expectedNames, DataSourceInitializedBuildItem.dataSourceNamesOf(buildItem)); + } + + @Test + @DisplayName("dataSourceNamesOf is null-safe and returns an empty set instead of null") + void testDataSourceNamesOfInstanceIsNullSafe() { + assertTrue(DataSourceInitializedBuildItem.dataSourceNamesOf(null).isEmpty()); + } + + @Test + @DisplayName("isDefaultDataSourcePresent matches if there is a default datasource") + void testIsDefaultDataSourcePresentMatches() { + Set expectedNames = new HashSet<>(Arrays.asList("", "one", "another")); + buildItem = DataSourceInitializedBuildItem.ofDefaultDataSourceAnd(expectedNames); + assertTrue(DataSourceInitializedBuildItem.isDefaultDataSourcePresent(buildItem)); + } + + @Test + @DisplayName("isDefaultDataSourcePresent is null-safe and returns false in case of null") + void testIsDefaultDataSourcePresentIsNullSafe() { + assertFalse(DataSourceInitializedBuildItem.isDefaultDataSourcePresent(null)); + } + + @Test + @DisplayName("default datasource is present") + void testOnlyDefaultDataSourcePresent() { + buildItem = new DataSourceInitializedBuildItem(Arrays.asList("any"), DEFAULT_DATASOURCE); + assertTrue(buildItem.isDefaultDataSourcePresent()); + } + + @Test + @DisplayName("default datasource is not present") + void testDefaultDataSourceIsNotPresent() { + buildItem = new DataSourceInitializedBuildItem(Arrays.asList("any"), NO_DEFAULT_DATASOURCE); + assertFalse(buildItem.isDefaultDataSourcePresent()); + } + + @Test + @DisplayName("createable with default datasource") + void testSubsequentlyAddedPresentOptionalDefaultDataSourceIsPresent() { + buildItem = DataSourceInitializedBuildItem.ofDefaultDataSourceAnd(Arrays.asList("notdefault")); + assertTrue(buildItem.isDefaultDataSourcePresent()); + } + + @Test + @DisplayName("createable without default datasource") + void testSubsequentlyAddedNonPresentOptionalDefaultDataSourceDoesNotChangeAnything() { + buildItem = DataSourceInitializedBuildItem.ofDataSources(Arrays.asList("notdefault")); + assertSame(buildItem, buildItem); + } + +} diff --git a/extensions/amazon-dynamodb/deployment/src/main/java/io/quarkus/dynamodb/deployment/DynamodbProcessor.java b/extensions/amazon-dynamodb/deployment/src/main/java/io/quarkus/dynamodb/deployment/DynamodbProcessor.java index 43ed8b1da59cc..a91c8481c8ccc 100644 --- a/extensions/amazon-dynamodb/deployment/src/main/java/io/quarkus/dynamodb/deployment/DynamodbProcessor.java +++ b/extensions/amazon-dynamodb/deployment/src/main/java/io/quarkus/dynamodb/deployment/DynamodbProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.dynamodb.deployment; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -81,14 +82,14 @@ void setup(CombinedIndexBuildItem combinedIndexBuildItem, // Indicates that this extension would like the SSL support to be enabled extensionSslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(FeatureBuildItem.DYNAMODB)); - INTERCEPTOR_PATHS.stream().forEach(path -> resource.produce(new NativeImageResourceBuildItem(path))); + INTERCEPTOR_PATHS.forEach(path -> resource.produce(new NativeImageResourceBuildItem(path))); List knownInterceptorImpls = combinedIndexBuildItem.getIndex() .getAllKnownImplementors(EXECUTION_INTERCEPTOR_NAME) .stream() .map(c -> c.name().toString()).collect(Collectors.toList()); - buildTimeConfig.sdk.interceptors.stream().forEach(interceptorClass -> { + buildTimeConfig.sdk.interceptors.orElse(Collections.emptyList()).forEach(interceptorClass -> { if (!knownInterceptorImpls.contains(interceptorClass.getName())) { throw new ConfigurationError( String.format( diff --git a/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientBrokenProxyConfigTest.java b/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientBrokenProxyConfigTest.java index 476fcb6633ea1..9e262b3136874 100644 --- a/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientBrokenProxyConfigTest.java +++ b/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientBrokenProxyConfigTest.java @@ -5,6 +5,7 @@ import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,6 +13,7 @@ import io.quarkus.test.QuarkusUnitTest; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +@Disabled("https://github.com/quarkusio/quarkus/issues/5286") public class DynamodbAsyncClientBrokenProxyConfigTest { @Inject diff --git a/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientTlsFileStoreConfigTest.java b/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientTlsFileStoreConfigTest.java index e7087cdec5b11..66143920fe989 100644 --- a/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientTlsFileStoreConfigTest.java +++ b/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbAsyncClientTlsFileStoreConfigTest.java @@ -4,12 +4,14 @@ import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +@Disabled("https://github.com/quarkusio/quarkus/issues/5286") public class DynamodbAsyncClientTlsFileStoreConfigTest { @Inject diff --git a/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbBrokenEndpointConfigTest.java b/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbBrokenEndpointConfigTest.java index 2d99e62bfcd22..ca9284f69ca88 100644 --- a/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbBrokenEndpointConfigTest.java +++ b/extensions/amazon-dynamodb/deployment/src/test/java/io/quarkus/dynamodb/deployment/DynamodbBrokenEndpointConfigTest.java @@ -5,6 +5,7 @@ import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -13,6 +14,7 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +@Disabled("https://github.com/quarkusio/quarkus/issues/5286") public class DynamodbBrokenEndpointConfigTest { @Inject diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderConfig.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderConfig.java index 47916fe636dfb..82a347729fb56 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderConfig.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderConfig.java @@ -22,7 +22,7 @@ public class AwsCredentialsProviderConfig { * ** Credential profiles file at the default location (`~/.aws/credentials`) shared by all AWS SDKs and the AWS CLI * ** Credentials delivered through the Amazon EC2 container service if `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` environment variable is set and security manager has permission to access the variable. * ** Instance profile credentials delivered through the Amazon EC2 metadata service - * * `static` - the provider that uses the access key and secret access key specified in the `tatic-provider` section of the config. + * * `static` - the provider that uses the access key and secret access key specified in the `static-provider` section of the config. * * `system-property` - it loads credentials from the `aws.accessKeyId`, `aws.secretAccessKey` and `aws.sessionToken` system properties. * * `env-variable` - it loads credentials from the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` environment variables. * * `profile` - credentials are based on AWS configuration profiles. This loads credentials from @@ -94,13 +94,13 @@ public static class StaticCredentialsProviderConfig { * AWS Access key id */ @ConfigItem - public String accessKeyId; + public Optional accessKeyId; /** * AWS Secret access key */ @ConfigItem - public String secretAccessKey; + public Optional secretAccessKey; } @ConfigGroup @@ -145,6 +145,6 @@ public static class ProcessCredentialsProviderConfig { * The command that should be executed to retrieve credentials. */ @ConfigItem - public String command; + public Optional command; } } diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderType.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderType.java index 8907be67bb409..7cbacef6973a0 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderType.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/AwsCredentialsProviderType.java @@ -26,7 +26,8 @@ public AwsCredentialsProvider create(AwsCredentialsProviderConfig config) { @Override public AwsCredentialsProvider create(AwsCredentialsProviderConfig config) { return StaticCredentialsProvider.create( - AwsBasicCredentials.create(config.staticProvider.accessKeyId, config.staticProvider.secretAccessKey)); + AwsBasicCredentials.create(config.staticProvider.accessKeyId.get(), + config.staticProvider.secretAccessKey.get())); } }, @@ -72,7 +73,7 @@ public AwsCredentialsProvider create(AwsCredentialsProviderConfig config) { builder.credentialRefreshThreshold(config.processProvider.credentialRefreshThreshold); builder.processOutputLimit(config.processProvider.processOutputLimit.asLongValue()); - builder.command(config.processProvider.command); + builder.command(config.processProvider.command.get()); return builder.build(); } diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/DynamodbClientProducer.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/DynamodbClientProducer.java index a0c73d547b29a..66975baa6fa55 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/DynamodbClientProducer.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/DynamodbClientProducer.java @@ -1,6 +1,7 @@ package io.quarkus.dynamodb.runtime; import java.net.URI; +import java.util.Collections; import java.util.HashSet; import java.util.Objects; @@ -91,15 +92,15 @@ private void initAwsClient(AwsClientBuilder builder, AwsConfig config) { config.region.ifPresent(builder::region); if (config.credentials.type == AwsCredentialsProviderType.STATIC) { - if (StringUtils.isBlank(config.credentials.staticProvider.accessKeyId) - || StringUtils.isBlank(config.credentials.staticProvider.secretAccessKey)) { + if (!config.credentials.staticProvider.accessKeyId.isPresent() + || !config.credentials.staticProvider.secretAccessKey.isPresent()) { throw new RuntimeConfigurationError( "quarkus.dynamodb.aws.credentials.static-provider.access-key-id and " + "quarkus.dynamodb.aws.credentials.static-provider.secret-access-key cannot be empty if STATIC credentials provider used."); } } if (config.credentials.type == AwsCredentialsProviderType.PROCESS) { - if (StringUtils.isBlank(config.credentials.processProvider.command)) { + if (!config.credentials.processProvider.command.isPresent()) { throw new RuntimeConfigurationError( "quarkus.dynamodb.aws.credentials.process-provider.command cannot be empty if PROCESS credentials provider used."); } @@ -121,7 +122,7 @@ private void initSdkClient(SdkClientBuilder builder, SdkConfig config) { ClientOverrideConfiguration.Builder overrides = ClientOverrideConfiguration.builder(); config.apiCallTimeout.ifPresent(overrides::apiCallTimeout); config.apiCallAttemptTimeout.ifPresent(overrides::apiCallAttemptTimeout); - buildTimeConfig.sdk.interceptors.stream() + buildTimeConfig.sdk.interceptors.orElse(Collections.emptyList()).stream() .map(this::createInterceptor) .filter(Objects::nonNull) .forEach(overrides::addExecutionInterceptor); @@ -163,12 +164,12 @@ private ApacheHttpClient.Builder createApacheClientBuilder(SyncHttpClientConfig builder.maxConnections(config.apache.maxConnections); builder.useIdleConnectionReaper(config.apache.useIdleConnectionReaper); - if (config.apache.proxy.enabled) { + if (config.apache.proxy.enabled && config.apache.proxy.endpoint.isPresent()) { ProxyConfiguration.Builder proxyBuilder = ProxyConfiguration.builder() - .endpoint(config.apache.proxy.endpoint); + .endpoint(config.apache.proxy.endpoint.get()); config.apache.proxy.username.ifPresent(proxyBuilder::username); config.apache.proxy.password.ifPresent(proxyBuilder::password); - config.apache.proxy.nonProxyHosts.forEach(proxyBuilder::addNonProxyHost); + config.apache.proxy.nonProxyHosts.ifPresent(c -> c.forEach(proxyBuilder::addNonProxyHost)); config.apache.proxy.ntlmDomain.ifPresent(proxyBuilder::ntlmDomain); config.apache.proxy.ntlmWorkstation.ifPresent(proxyBuilder::ntlmWorkstation); config.apache.proxy.preemptiveBasicAuthenticationEnabled @@ -199,14 +200,14 @@ private NettyNioAsyncHttpClient.Builder createNettyClientBuilder(NettyHttpClient config.sslProvider.ifPresent(builder::sslProvider); builder.useIdleConnectionReaper(config.useIdleConnectionReaper); - if (config.proxy.enabled) { + if (config.proxy.enabled && config.proxy.endpoint.isPresent()) { software.amazon.awssdk.http.nio.netty.ProxyConfiguration.Builder proxyBuilder = software.amazon.awssdk.http.nio.netty.ProxyConfiguration - .builder().scheme(config.proxy.endpoint.getScheme()) - .host(config.proxy.endpoint.getHost()) - .nonProxyHosts(new HashSet<>(config.proxy.nonProxyHosts)); + .builder().scheme(config.proxy.endpoint.get().getScheme()) + .host(config.proxy.endpoint.get().getHost()) + .nonProxyHosts(new HashSet<>(config.proxy.nonProxyHosts.orElse(Collections.emptyList()))); - if (config.proxy.endpoint.getPort() != -1) { - proxyBuilder.port(config.proxy.endpoint.getPort()); + if (config.proxy.endpoint.get().getPort() != -1) { + proxyBuilder.port(config.proxy.endpoint.get().getPort()); } builder.proxyConfiguration(proxyBuilder.build()); } @@ -239,11 +240,8 @@ private void validateApacheClientConfig(SyncHttpClientConfig config) { if (config.apache.maxConnections <= 0) { throw new RuntimeConfigurationError("quarkus.dynamodb.sync-client.max-connections may not be negative or zero."); } - if (config.apache.proxy != null && config.apache.proxy.enabled) { - URI proxyEndpoint = config.apache.proxy.endpoint; - if (proxyEndpoint != null) { - validateProxyEndpoint(proxyEndpoint, "sync"); - } + if (config.apache.proxy.enabled) { + config.apache.proxy.endpoint.ifPresent(u -> validateProxyEndpoint(u, "sync")); } validateTlsManagersProvider(config.apache.tlsManagersProvider, "sync"); } @@ -266,11 +264,8 @@ private void validateNettyClientConfig(NettyHttpClientConfig asyncClient) { "quarkus.dynamodb.async-client.event-loop.number-of-threads may not be negative or zero."); } } - if (asyncClient.proxy != null && asyncClient.proxy.enabled) { - URI proxyEndpoint = asyncClient.proxy.endpoint; - if (proxyEndpoint != null) { - validateProxyEndpoint(proxyEndpoint, "async"); - } + if (asyncClient.proxy.enabled) { + asyncClient.proxy.endpoint.ifPresent(proxyEndpoint -> validateProxyEndpoint(proxyEndpoint, "async")); } validateTlsManagersProvider(asyncClient.tlsManagersProvider, "async"); } @@ -310,30 +305,12 @@ private void validateProxyEndpoint(URI endpoint, String clientType) { private void validateTlsManagersProvider(TlsManagersProviderConfig config, String clientType) { if (config.type == TlsManagersProviderType.FILE_STORE) { - if (config.fileStore == null) { + if (!config.fileStore.isPresent()) { throw new RuntimeConfigurationError( String.format( "quarkus.dynamodb.%s-client.tls-managers-provider.file-store must be specified if 'FILE_STORE' provider type is used", clientType)); } - if (config.fileStore.path == null) { - throw new RuntimeConfigurationError( - String.format( - "quarkus.dynamodb.%s-client.tls-managers-provider.file-store.path should not be empty if 'FILE_STORE' provider is used.", - clientType)); - } - if (StringUtils.isBlank(config.fileStore.type)) { - throw new RuntimeConfigurationError( - String.format( - "quarkus.dynamodb.%s-client.tls-managers-provider.file-store.type should not be empty if 'FILE_STORE' provider is used.", - clientType)); - } - if (StringUtils.isBlank(config.fileStore.password)) { - throw new RuntimeConfigurationError( - String.format( - "quarkus.dynamodb.%s-client.tls-managers-provider.file-store.password should not be empty if 'FILE_STORE' provider is used.", - clientType)); - } } } } diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/NettyHttpClientConfig.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/NettyHttpClientConfig.java index 7902e13a4c4b8..fdc93e00745a0 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/NettyHttpClientConfig.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/NettyHttpClientConfig.java @@ -168,13 +168,13 @@ public static class NettyProxyConfiguration { * raised. */ @ConfigItem - public URI endpoint; + public Optional endpoint; /** * The hosts that the client is allowed to access without going through the proxy. */ @ConfigItem - public List nonProxyHosts; + public Optional> nonProxyHosts; } //TODO: additionalChannelOptions diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SdkBuildTimeConfig.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SdkBuildTimeConfig.java index fb2ac2204a3f4..214642e32832c 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SdkBuildTimeConfig.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SdkBuildTimeConfig.java @@ -1,6 +1,7 @@ package io.quarkus.dynamodb.runtime; import java.util.List; +import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -20,5 +21,5 @@ public class SdkBuildTimeConfig { * @see software.amazon.awssdk.core.interceptor.ExecutionInterceptor */ @ConfigItem - public List> interceptors; + public Optional>> interceptors; } diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SyncHttpClientConfig.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SyncHttpClientConfig.java index ca3a9b6c9dedf..31607560615bb 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SyncHttpClientConfig.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/SyncHttpClientConfig.java @@ -103,7 +103,7 @@ public static class HttpClientProxyConfiguration { * raised. */ @ConfigItem - public URI endpoint; + public Optional endpoint; /** * The username to use when connecting through a proxy. @@ -139,7 +139,7 @@ public static class HttpClientProxyConfiguration { * The hosts that the client is allowed to access without going through the proxy. */ @ConfigItem - public List nonProxyHosts; + public Optional> nonProxyHosts; } } } diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderConfig.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderConfig.java index 2b3612053f874..c30a6a6a25d91 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderConfig.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderConfig.java @@ -1,6 +1,7 @@ package io.quarkus.dynamodb.runtime; import java.nio.file.Path; +import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -32,7 +33,7 @@ public class TlsManagersProviderConfig { * Used only if {@code FILE_STORE} type is chosen. */ @ConfigItem - public FileStoreTlsManagersProviderConfig fileStore; + public Optional fileStore; @ConfigGroup public static class FileStoreTlsManagersProviderConfig { diff --git a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderType.java b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderType.java index 84f82da15377e..e9960cd350e55 100644 --- a/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderType.java +++ b/extensions/amazon-dynamodb/runtime/src/main/java/io/quarkus/dynamodb/runtime/TlsManagersProviderType.java @@ -20,8 +20,9 @@ public TlsKeyManagersProvider create(TlsManagersProviderConfig config) { FILE_STORE { @Override public TlsKeyManagersProvider create(TlsManagersProviderConfig config) { - return FileStoreTlsKeyManagersProvider.create(config.fileStore.path, config.fileStore.type, - config.fileStore.password); + final TlsManagersProviderConfig.FileStoreTlsManagersProviderConfig fileStore = config.fileStore.get(); + return FileStoreTlsKeyManagersProvider.create(fileStore.path, fileStore.type, + fileStore.password); } }; diff --git a/extensions/amazon-lambda-http/maven-archetype/pom.xml b/extensions/amazon-lambda-http/maven-archetype/pom.xml index 674bd13dd27e9..93afd0c19456f 100644 --- a/extensions/amazon-lambda-http/maven-archetype/pom.xml +++ b/extensions/amazon-lambda-http/maven-archetype/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-amazon-lambda-http-archetype - Quarkus - HTTP Amazon Lambda Archetype + Quarkus - Amazon Lambda HTTP - Archetype maven-archetype @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/extensions/amazon-lambda-http/runtime/pom.xml b/extensions/amazon-lambda-http/runtime/pom.xml index 42509a4a966ae..9dc609d58a669 100644 --- a/extensions/amazon-lambda-http/runtime/pom.xml +++ b/extensions/amazon-lambda-http/runtime/pom.xml @@ -28,6 +28,10 @@ io.quarkus quarkus-core + + com.amazonaws.serverless + aws-serverless-java-container-core + com.oracle.substratevm svm @@ -54,4 +58,4 @@ - \ No newline at end of file + diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java index 2169542d9e1f9..7481a03e8572c 100644 --- a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -144,8 +145,12 @@ private AwsProxyResponse nettyDispatch(VirtualClientConnection connection, AwsPr } if (msg instanceof LastHttpContent) { if (baos != null) { - responseBuilder.setBase64Encoded(true); - responseBuilder.setBody(Base64.getMimeEncoder().encodeToString(baos.toByteArray())); + if (isBinary(responseBuilder.getMultiValueHeaders().getFirst("Content-Type"))) { + responseBuilder.setBase64Encoded(true); + responseBuilder.setBody(Base64.getMimeEncoder().encodeToString(baos.toByteArray())); + } else { + responseBuilder.setBody(new String(baos.toByteArray(), "UTF-8")); + } } return responseBuilder; } @@ -156,4 +161,16 @@ private AwsProxyResponse nettyDispatch(VirtualClientConnection connection, AwsPr } } + private boolean isBinary(String contentType) { + if (contentType != null) { + int index = contentType.indexOf(';'); + if (index >= 0) { + return LambdaContainerHandler.getContainerConfig().isBinaryContentType(contentType.substring(0, index)); + } else { + return LambdaContainerHandler.getContainerConfig().isBinaryContentType(contentType); + } + } + return false; + } + } diff --git a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java index 70ea6982a82cb..e5ad77550273f 100644 --- a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java +++ b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java @@ -21,6 +21,7 @@ import io.quarkus.amazon.lambda.runtime.AmazonLambdaRecorder; import io.quarkus.amazon.lambda.runtime.FunctionError; +import io.quarkus.amazon.lambda.runtime.LambdaBuildTimeConfig; import io.quarkus.amazon.lambda.runtime.LambdaConfig; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; @@ -196,7 +197,7 @@ void ipv4Only(BuildProducer systemProperty) { @BuildStep @Record(value = ExecutionTime.RUNTIME_INIT) - void enableNativeEventLoop(LambdaConfig config, + void enableNativeEventLoop(LambdaBuildTimeConfig config, AmazonLambdaRecorder recorder, List orderServicesFirst, // force some ordering of recorders ShutdownContextBuildItem shutdownContextBuildItem, diff --git a/extensions/amazon-lambda/maven-archetype/pom.xml b/extensions/amazon-lambda/maven-archetype/pom.xml index 2f9d454bdf47a..632b6f3d6f762 100644 --- a/extensions/amazon-lambda/maven-archetype/pom.xml +++ b/extensions/amazon-lambda/maven-archetype/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-amazon-lambda-archetype - Quarkus - Amazon Lambda Archetype + Quarkus - Amazon Lambda - Archetype maven-archetype @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/main/java/TestLambda.java b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/main/java/TestLambda.java index eeb61e79975bb..167b7f25510b2 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/main/java/TestLambda.java +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/main/java/TestLambda.java @@ -14,6 +14,6 @@ public class TestLambda implements RequestHandler { @Override public OutputObject handleRequest(InputObject input, Context context) { - return service.proces(input).setRequestId(context.getAwsRequestId()); + return service.process(input).setRequestId(context.getAwsRequestId()); } } diff --git a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java new file mode 100644 index 0000000000000..3af310748aa10 --- /dev/null +++ b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java @@ -0,0 +1,20 @@ +package io.quarkus.amazon.lambda.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * + */ +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public class LambdaBuildTimeConfig { + + /** + * If true, this will enable the aws event poll loop within a Quarkus test run. This loop normally only runs in native + * image. This option is strictly for testing purposes. + * + */ + @ConfigItem + public boolean enablePollingJvmMode; +} diff --git a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaConfig.java b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaConfig.java index dd5aa8ecb5f95..c22d3462f2bf4 100644 --- a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaConfig.java +++ b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaConfig.java @@ -20,13 +20,4 @@ public class LambdaConfig { */ @ConfigItem public Optional handler; - - /** - * If true, this will enable the aws event poll loop within a Quarkus test run. This loop normally only runs in native - * image. This option is strictly for testing purposes. - * - */ - @ConfigItem - public boolean enablePollingJvmMode; - } diff --git a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/QuarkusStreamHandler.java b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/QuarkusStreamHandler.java index d9fe96653c07a..5e5cb505de1d4 100644 --- a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/QuarkusStreamHandler.java +++ b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/QuarkusStreamHandler.java @@ -30,6 +30,12 @@ public class QuarkusStreamHandler implements RequestStreamHandler { Class appClass = Class.forName("io.quarkus.runner.ApplicationImpl"); String[] args = {}; Application app = (Application) appClass.newInstance(); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + app.stop(); + } + }); app.start(args); errorWriter.println("Quarkus bootstrapped successfully."); started = true; diff --git a/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml index b5bd36a227634..ee534812a3b46 100644 --- a/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -7,4 +7,5 @@ metadata: - "amazon" categories: - "cloud" + guide: "https://quarkus.io/guides/amazon-lambda" status: "preview" diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java index 72fd88468b7e4..a2f1c5c821c8c 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java @@ -7,6 +7,7 @@ import java.util.HashSet; import java.util.Set; +import io.quarkus.arc.config.ConfigProperties; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -60,6 +61,13 @@ public class ArcConfig { @ConfigItem(defaultValue = "true") public boolean removeFinalForProxyableMethods; + /** + * The default naming strategy for {@link ConfigProperties.NamingStrategy}. The allowed values are determined + * by that enum + */ + @ConfigItem(defaultValue = "kebab-case") + public ConfigProperties.NamingStrategy configPropertiesDefaultNamingStrategy; + public final boolean isRemoveUnusedBeansFieldValid() { return ALLOWED_REMOVE_UNUSED_BEANS_VALUES.contains(removeUnusedBeans.toLowerCase()); } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index c46dc8d18a281..5c980a5cccb2b 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -97,6 +97,7 @@ public ContextRegistrationPhaseBuildItem initialize( ApplicationArchivesBuildItem applicationArchivesBuildItem, List annotationTransformers, List injectionPointTransformers, + List observerTransformers, List interceptorBindingRegistrarBuildItems, List additionalStereotypeBuildItems, List applicationClassPredicates, @@ -108,6 +109,7 @@ public ContextRegistrationPhaseBuildItem initialize( List additionalBeanDefiningAnnotations, List removalExclusions, Optional testClassPredicate, + Capabilities capabilities, BuildProducer feature) { if (!arcConfig.isRemoveUnusedBeansFieldValid()) { @@ -172,12 +174,16 @@ public void transform(TransformationContext transformationContext) { builder.addResourceAnnotations( resourceAnnotations.stream().map(ResourceAnnotationBuildItem::getName).collect(Collectors.toList())); // register all annotation transformers - for (AnnotationsTransformerBuildItem transformerItem : annotationTransformers) { - builder.addAnnotationTransformer(transformerItem.getAnnotationsTransformer()); + for (AnnotationsTransformerBuildItem transformer : annotationTransformers) { + builder.addAnnotationTransformer(transformer.getAnnotationsTransformer()); } // register all injection point transformers - for (InjectionPointTransformerBuildItem transformerItem : injectionPointTransformers) { - builder.addInjectionPointTransformer(transformerItem.getInjectionPointsTransformer()); + for (InjectionPointTransformerBuildItem transformer : injectionPointTransformers) { + builder.addInjectionPointTransformer(transformer.getInjectionPointsTransformer()); + } + // register all observer transformers + for (ObserverTransformerBuildItem transformer : observerTransformers) { + builder.addObserverTransformer(transformer.getInstance()); } // register additional interceptor bindings for (InterceptorBindingRegistrarBuildItem bindingRegistrar : interceptorBindingRegistrarBuildItems) { @@ -227,6 +233,7 @@ public boolean test(BeanInfo bean) { }); } builder.setRemoveFinalFromProxyableMethods(arcConfig.removeFinalForProxyableMethods); + builder.setJtaCapabilities(capabilities.isCapabilityPresent(Capabilities.TRANSACTIONS)); BeanProcessor beanProcessor = builder.build(); ContextRegistrar.RegistrationContext context = beanProcessor.registerCustomContexts(); diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java index cf66f9e1e516b..b1bebc9675a57 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java @@ -27,11 +27,11 @@ import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.arc.runtime.ConfigBeanCreator; import io.quarkus.arc.runtime.ConfigRecorder; -import io.quarkus.arc.runtime.QuarkusConfigProducer; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.smallrye.config.inject.ConfigProducer; /** * MicroProfile Config related build steps. @@ -44,7 +44,7 @@ public class ConfigBuildStep { @BuildStep AdditionalBeanBuildItem bean() { - return new AdditionalBeanBuildItem(QuarkusConfigProducer.class); + return new AdditionalBeanBuildItem(ConfigProducer.class); } @BuildStep diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ObserverTransformerBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ObserverTransformerBuildItem.java new file mode 100644 index 0000000000000..296cd1a527630 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ObserverTransformerBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.arc.deployment; + +import io.quarkus.arc.processor.ObserverTransformer; +import io.quarkus.builder.item.MultiBuildItem; + +/** + * This build item is used to register an {@link ObserverTransformer} instance. + */ +public final class ObserverTransformerBuildItem extends MultiBuildItem { + + private final ObserverTransformer transformer; + + public ObserverTransformerBuildItem(ObserverTransformer transformer) { + this.transformer = transformer; + } + + public ObserverTransformer getInstance() { + return transformer; + } +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ObserverValidationProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ObserverValidationProcessor.java new file mode 100644 index 0000000000000..1f9efcaf8b74b --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ObserverValidationProcessor.java @@ -0,0 +1,59 @@ +package io.quarkus.arc.deployment; + +import java.util.Collection; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.logging.Logger; + +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.BeanDeploymentValidator; +import io.quarkus.arc.processor.BuiltinScope; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.arc.processor.ObserverInfo; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; + +/** + * Validates observer methods from application classes. + * If an observer listening for {@code @Initialized(ApplicationScoped.class)} is found, it logs a warning. + */ +public class ObserverValidationProcessor { + + private static final Logger LOGGER = Logger.getLogger(ObserverValidationProcessor.class.getName()); + + @BuildStep + public void validateApplicationObserver(ApplicationArchivesBuildItem applicationArchivesBuildItem, + BuildProducer validators) { + // an index of all root archive classes (usually src/main/classes) + IndexView applicationClassesIndex = applicationArchivesBuildItem.getRootArchive().getIndex(); + + validators.produce(new BeanDeploymentValidatorBuildItem(new BeanDeploymentValidator() { + + @Override + public void validate(ValidationContext context) { + Collection allObservers = context.get(Key.OBSERVERS); + // do the validation for each observer that can be found within application classes + for (ObserverInfo observer : allObservers) { + DotName declaringBeanDotName = observer.getDeclaringBean().getBeanClass(); + AnnotationInstance instance = Annotations.getParameterAnnotation(observer.getObserverMethod(), + DotNames.INITIALIZED); + if (applicationClassesIndex.getClassByName(declaringBeanDotName) != null && instance != null && + instance.value().asClass().name().equals(BuiltinScope.APPLICATION.getName())) { + // found an observer for @Initialized(ApplicationScoped.class) + // log a warning and recommend to use StartupEvent instead + final String observerWarning = "The method %s#%s is an observer for " + + "@Initialized(ApplicationScoped.class). Observer notification for this event may " + + "vary between JVM and native modes! We strongly recommend to observe StartupEvent " + + "instead as that one is consistently delivered in both modes once the container is " + + "running."; + LOGGER.warnf(observerWarning, observer.getDeclaringBean().getImplClazz(), + observer.getObserverMethod().name()); + } + } + } + })); + } +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ClassConfigPropertiesUtil.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ClassConfigPropertiesUtil.java index 405f96a02f269..c272449642270 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ClassConfigPropertiesUtil.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ClassConfigPropertiesUtil.java @@ -25,6 +25,7 @@ import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; +import io.quarkus.arc.config.ConfigProperties; import io.quarkus.arc.deployment.ConfigPropertyBuildItem; import io.quarkus.arc.deployment.configproperties.ConfigPropertiesUtil.ReadOptionalResponse; import io.quarkus.deployment.annotations.BuildProducer; @@ -115,7 +116,8 @@ static void generateStartupObserverThatInjectsConfigClass(ClassOutput classOutpu * @return true if the configuration class needs validation */ static boolean addProducerMethodForClassConfigProperties(ClassLoader classLoader, ClassInfo configPropertiesClassInfo, - ClassCreator producerClassCreator, String prefixStr, IndexView applicationIndex, + ClassCreator producerClassCreator, String prefixStr, ConfigProperties.NamingStrategy namingStrategy, + IndexView applicationIndex, BuildProducer configProperties) { if (!DotNames.OBJECT.equals(configPropertiesClassInfo.superName())) { @@ -167,8 +169,8 @@ static boolean addProducerMethodForClassConfigProperties(ClassLoader classLoader configObjectClassStr, produceMethodParameterTypes)) { methodCreator.addAnnotation(Produces.class); - ResultHandle configObject = populateConfigObject(classLoader, configPropertiesClassInfo, prefixStr, methodCreator, - applicationIndex, configProperties); + ResultHandle configObject = populateConfigObject(classLoader, configPropertiesClassInfo, prefixStr, namingStrategy, + methodCreator, applicationIndex, configProperties); if (needsValidation) { createValidationCodePath(methodCreator, configObject, prefixStr); @@ -198,7 +200,8 @@ private static boolean isHibernateValidatorInClasspath() { } private static ResultHandle populateConfigObject(ClassLoader classLoader, ClassInfo configClassInfo, String prefixStr, - MethodCreator methodCreator, IndexView applicationIndex, BuildProducer configProperties) { + ConfigProperties.NamingStrategy namingStrategy, MethodCreator methodCreator, IndexView applicationIndex, + BuildProducer configProperties) { String configObjectClassStr = configClassInfo.name().toString(); ResultHandle configObject = methodCreator.newInstance(MethodDescriptor.ofConstructor(configObjectClassStr)); @@ -245,11 +248,12 @@ private static ResultHandle populateConfigObject(ClassLoader classLoader, ClassI } ResultHandle nestedConfigObject = populateConfigObject(classLoader, fieldTypeClassInfo, - prefixStr + "." + field.name(), methodCreator, applicationIndex, configProperties); + prefixStr + "." + namingStrategy.getName(field.name()), namingStrategy, methodCreator, + applicationIndex, configProperties); createWriteValue(methodCreator, configObject, field, setter, useFieldAccess, nestedConfigObject); } else { - String fullConfigName = prefixStr + "." + field.name(); + String fullConfigName = prefixStr + "." + namingStrategy.getName(field.name()); ResultHandle config = methodCreator.getMethodParam(0); if (DotNames.OPTIONAL.equals(fieldTypeDotName)) { Type genericType = determineSingleGenericType(field.type(), diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java index 547f39fe2640f..c698b1b078cd9 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesBuildStep.java @@ -1,24 +1,17 @@ package io.quarkus.arc.deployment.configproperties; -import static io.quarkus.runtime.util.StringUtil.camelHumpsIterator; -import static io.quarkus.runtime.util.StringUtil.join; -import static io.quarkus.runtime.util.StringUtil.lowerCase; -import static io.quarkus.runtime.util.StringUtil.withoutSuffix; - import java.lang.reflect.Modifier; -import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.inject.Singleton; import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.IndexView; -import io.quarkus.arc.config.ConfigProperties; +import io.quarkus.arc.deployment.ArcConfig; import io.quarkus.arc.deployment.ConfigPropertyBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; @@ -35,17 +28,26 @@ public class ConfigPropertiesBuildStep { + @BuildStep + void produceConfigPropertiesMetadata(CombinedIndexBuildItem combinedIndex, ArcConfig arcConfig, + BuildProducer configPropertiesMetadataProducer) { + for (AnnotationInstance annotation : combinedIndex.getIndex().getAnnotations(DotNames.CONFIG_PROPERTIES)) { + configPropertiesMetadataProducer + .produce( + new ConfigPropertiesMetadataBuildItem(annotation, arcConfig.configPropertiesDefaultNamingStrategy)); + } + } + @BuildStep void setup(CombinedIndexBuildItem combinedIndex, ApplicationIndexBuildItem applicationIndex, + List configPropertiesMetadataList, BuildProducer generatedClasses, BuildProducer generatedBeans, BuildProducer defaultConfigValues, BuildProducer configProperties, DeploymentClassLoaderBuildItem deploymentClassLoader) { - IndexView index = combinedIndex.getIndex(); - Collection instances = index.getAnnotations(DotNames.CONFIG_PROPERTIES); - if (instances.isEmpty()) { + if (configPropertiesMetadataList.isEmpty()) { return; } @@ -62,11 +64,10 @@ void setup(CombinedIndexBuildItem combinedIndex, .build(); producerClassCreator.addAnnotation(Singleton.class); - Set configClassesThatNeedValidation = new HashSet<>(instances.size()); - for (AnnotationInstance configPropertiesInstance : instances) { - ClassInfo classInfo = configPropertiesInstance.target().asClass(); + Set configClassesThatNeedValidation = new HashSet<>(configPropertiesMetadataList.size()); + for (ConfigPropertiesMetadataBuildItem configPropertiesMetadata : configPropertiesMetadataList) { + ClassInfo classInfo = configPropertiesMetadata.getClassInfo(); - String prefixStr = determinePrefix(configPropertiesInstance); if (Modifier.isInterface(classInfo.flags())) { /* * In this case we need to generate an implementation of the interface that for each interface method @@ -75,8 +76,8 @@ void setup(CombinedIndexBuildItem combinedIndex, */ String generatedClassName = InterfaceConfigPropertiesUtil.generateImplementationForInterfaceConfigProperties( - classInfo, nonBeansClassOutput, index, prefixStr, - defaultConfigValues, configProperties); + classInfo, nonBeansClassOutput, combinedIndex.getIndex(), configPropertiesMetadata.getPrefix(), + configPropertiesMetadata.getNamingStrategy(), defaultConfigValues, configProperties); InterfaceConfigPropertiesUtil.addProducerMethodForInterfaceConfigProperties(producerClassCreator, classInfo.name(), generatedClassName); @@ -86,7 +87,8 @@ void setup(CombinedIndexBuildItem combinedIndex, * and call setters for value obtained from MP Config */ boolean needsValidation = ClassConfigPropertiesUtil.addProducerMethodForClassConfigProperties( - deploymentClassLoader.getClassLoader(), classInfo, producerClassCreator, prefixStr, + deploymentClassLoader.getClassLoader(), classInfo, producerClassCreator, + configPropertiesMetadata.getPrefix(), configPropertiesMetadata.getNamingStrategy(), applicationIndex.getIndex(), configProperties); if (needsValidation) { configClassesThatNeedValidation.add(classInfo.name()); @@ -101,35 +103,4 @@ void setup(CombinedIndexBuildItem combinedIndex, configClassesThatNeedValidation); } } - - /** - * Use the annotation value - */ - private String determinePrefix(AnnotationInstance configPropertiesInstance) { - String fromAnnotation = getPrefixFromAnnotation(configPropertiesInstance); - if (fromAnnotation != null) { - return fromAnnotation; - } - return getPrefixFromClassName(configPropertiesInstance.target().asClass().name()); - } - - private String getPrefixFromAnnotation(AnnotationInstance configPropertiesInstance) { - AnnotationValue annotationValue = configPropertiesInstance.value("prefix"); - if (annotationValue == null) { - return null; - } - String value = annotationValue.asString(); - if (ConfigProperties.UNSET_PREFIX.equals(value) || value.isEmpty()) { - return null; - } - return value; - } - - private String getPrefixFromClassName(DotName className) { - String simpleName = className.isInner() ? className.local() : className.withoutPackagePrefix(); - return join("-", - withoutSuffix(lowerCase(camelHumpsIterator(simpleName)), "config", "configuration", - "properties", "props")); - } - } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesMetadataBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesMetadataBuildItem.java new file mode 100644 index 0000000000000..8068d764837b0 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ConfigPropertiesMetadataBuildItem.java @@ -0,0 +1,77 @@ +package io.quarkus.arc.deployment.configproperties; + +import static io.quarkus.runtime.util.StringUtil.camelHumpsIterator; +import static io.quarkus.runtime.util.StringUtil.join; +import static io.quarkus.runtime.util.StringUtil.lowerCase; +import static io.quarkus.runtime.util.StringUtil.withoutSuffix; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.config.ConfigProperties; +import io.quarkus.builder.item.MultiBuildItem; + +public final class ConfigPropertiesMetadataBuildItem extends MultiBuildItem { + + private static final DotName CONFIG_PROPERTIES_ANNOTATION = DotName.createSimple(ConfigProperties.class.getName()); + + private final ClassInfo classInfo; + private final String prefix; + private final ConfigProperties.NamingStrategy namingStrategy; + + public ConfigPropertiesMetadataBuildItem(AnnotationInstance annotation, ConfigProperties.NamingStrategy defaultStrategy) { + if (!CONFIG_PROPERTIES_ANNOTATION.equals(annotation.name())) { + throw new IllegalArgumentException(annotation + " is not an instance of " + ConfigProperties.class.getSimpleName()); + } + + this.classInfo = annotation.target().asClass(); + this.prefix = extractPrefix(annotation); + AnnotationValue namingStrategyValue = annotation.value("namingStrategy"); + this.namingStrategy = namingStrategyValue == null ? defaultStrategy + : ConfigProperties.NamingStrategy.valueOf(namingStrategyValue.asEnum()); + } + + public ConfigPropertiesMetadataBuildItem(ClassInfo classInfo, String prefix, + ConfigProperties.NamingStrategy namingStrategy) { + this.classInfo = classInfo; + this.prefix = sanitisePrefix(prefix); + this.namingStrategy = namingStrategy; + } + + public ClassInfo getClassInfo() { + return classInfo; + } + + public String getPrefix() { + return prefix; + } + + public ConfigProperties.NamingStrategy getNamingStrategy() { + return namingStrategy; + } + + private String extractPrefix(AnnotationInstance annotationInstance) { + AnnotationValue value = annotationInstance.value("prefix"); + return sanitisePrefix(value == null ? null : value.asString()); + } + + private String sanitisePrefix(String prefix) { + if (isPrefixUnset(prefix)) { + return getPrefixFromClassName(classInfo.name()); + } + return prefix; + } + + private boolean isPrefixUnset(String prefix) { + return prefix == null || "".equals(prefix.trim()) || ConfigProperties.UNSET_PREFIX.equals(prefix.trim()); + } + + private String getPrefixFromClassName(DotName className) { + String simpleName = className.isInner() ? className.local() : className.withoutPackagePrefix(); + return join("-", + withoutSuffix(lowerCase(camelHumpsIterator(simpleName)), "config", "configuration", + "properties", "props")); + } +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/InterfaceConfigPropertiesUtil.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/InterfaceConfigPropertiesUtil.java index 988cdf61a4465..bd02a980f3428 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/InterfaceConfigPropertiesUtil.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/InterfaceConfigPropertiesUtil.java @@ -21,6 +21,7 @@ import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; +import io.quarkus.arc.config.ConfigProperties; import io.quarkus.arc.deployment.ConfigPropertyBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.bean.JavaBeanUtil; @@ -61,7 +62,8 @@ static void addProducerMethodForInterfaceConfigProperties(ClassCreator classCrea } static String generateImplementationForInterfaceConfigProperties(ClassInfo originalInterface, ClassOutput classOutput, - IndexView index, String prefixStr, BuildProducer defaultConfigValues, + IndexView index, String prefixStr, ConfigProperties.NamingStrategy namingStrategy, + BuildProducer defaultConfigValues, BuildProducer configProperties) { Set allInterfaces = new HashSet<>(); allInterfaces.add(originalInterface.name()); @@ -109,7 +111,7 @@ static String generateImplementationForInterfaceConfigProperties(ClassInfo origi * and instead just rely on what MP Config gives us back */ - NameAndDefaultValue nameAndDefaultValue = determinePropertyNameAndDefaultValue(method); + NameAndDefaultValue nameAndDefaultValue = determinePropertyNameAndDefaultValue(method, namingStrategy); String fullConfigName = prefixStr + "." + nameAndDefaultValue.getName(); try (MethodCreator methodCreator = interfaceImplClassCreator.getMethodCreator(method.name(), method.returnType().name().toString())) { @@ -196,27 +198,29 @@ private static boolean isDefault(short flags) { return ((flags & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC); } - private static NameAndDefaultValue determinePropertyNameAndDefaultValue(MethodInfo method) { + private static NameAndDefaultValue determinePropertyNameAndDefaultValue(MethodInfo method, + ConfigProperties.NamingStrategy namingStrategy) { AnnotationInstance configPropertyAnnotation = method.annotation(DotNames.CONFIG_PROPERTY); if (configPropertyAnnotation != null) { AnnotationValue nameValue = configPropertyAnnotation.value("name"); - String name = (nameValue == null) || nameValue.asString().isEmpty() ? getPropertyNameFromMethodName(method) + String name = (nameValue == null) || nameValue.asString().isEmpty() ? getPropertyName(method, namingStrategy) : nameValue.asString(); AnnotationValue defaultValue = configPropertyAnnotation.value("defaultValue"); return new NameAndDefaultValue(name, defaultValue != null ? defaultValue.asString() : null); } - return new NameAndDefaultValue(getPropertyNameFromMethodName(method)); + return new NameAndDefaultValue(getPropertyName(method, namingStrategy)); } - private static String getPropertyNameFromMethodName(MethodInfo method) { + private static String getPropertyName(MethodInfo method, ConfigProperties.NamingStrategy namingStrategy) { + String effectiveName = method.name(); try { - return JavaBeanUtil.getPropertyNameFromGetter(method.name()); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Method " + method.name() + " of interface " + method.declaringClass() - + " is not a getter method. Either rename the method to follow getter name conventions or annotate the method with @ConfigProperty"); + effectiveName = JavaBeanUtil.getPropertyNameFromGetter(method.name()); + } catch (IllegalArgumentException ignored) { + } + return namingStrategy.getName(effectiveName); } private static class NameAndDefaultValue { diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/ClassWithAllPublicFieldsConfigPropertiesTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/ClassWithAllPublicFieldsConfigPropertiesTest.java index 0907f4258eea5..65d79dff90e20 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/ClassWithAllPublicFieldsConfigPropertiesTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/ClassWithAllPublicFieldsConfigPropertiesTest.java @@ -27,7 +27,7 @@ public class ClassWithAllPublicFieldsConfigPropertiesTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(DummyBean.class, DummyProperties.class) .addAsResource(new StringAsset( - "dummy.name=quarkus\ndummy.numbers=1,2,3,4\ndummy.boolWithDefault=true\ndummy.optionalInt=100"), + "dummy.name=quarkus\ndummy.numbers=1,2,3,4\ndummy.bool-with-default=true\ndummy.optional-int=100"), "application.properties")); @Inject diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/FromConfigConfigDefaultConfigPropertiesTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/FromConfigConfigDefaultConfigPropertiesTest.java new file mode 100644 index 0000000000000..3eb5d2d97874a --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/FromConfigConfigDefaultConfigPropertiesTest.java @@ -0,0 +1,80 @@ +package io.quarkus.arc.test.configproperties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.config.ConfigProperties; +import io.quarkus.test.QuarkusUnitTest; + +public class FromConfigConfigDefaultConfigPropertiesTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(DummyBean.class, DummyProperties.class) + .addAsResource(new StringAsset( + "quarkus.arc.config-properties-default-naming-strategy=verbatim\ndummy.fooBarDev=quarkus1\ndummy2.foo-bar=quarkus2"), + "application.properties")); + + @Inject + DummyBean dummyBean; + + @Test + public void testConfiguredValues() { + assertEquals("quarkus2", dummyBean.getFooBar()); + assertEquals("quarkus1", dummyBean.getFooBarDev()); + } + + @Singleton + public static class DummyBean { + @Inject + DummyProperties dummyProperties; + + @Inject + DummyProperties2 dummyProperties2; + + String getFooBar() { + return dummyProperties2.getFooBar(); + } + + String getFooBarDev() { + return dummyProperties.getFooBarDev(); + } + } + + @ConfigProperties(prefix = "dummy") + public static class DummyProperties { + + public String fooBarDev; + + public String getFooBarDev() { + return fooBarDev; + } + + public void setFooBarDev(String fooBarDev) { + this.fooBarDev = fooBarDev; + } + } + + @ConfigProperties(prefix = "dummy2", namingStrategy = ConfigProperties.NamingStrategy.KEBAB_CASE) + public static class DummyProperties2 { + + public String fooBar; + + public String getFooBar() { + return fooBar; + } + + public void setFooBar(String fooBar) { + this.fooBar = fooBar; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalClassConfigPropertiesTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalClassConfigPropertiesTest.java index a2bbc838f6e33..93e732fc0ba56 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalClassConfigPropertiesTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalClassConfigPropertiesTest.java @@ -27,7 +27,7 @@ public class TypicalClassConfigPropertiesTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(DummyBean.class, DummyProperties.class) .addAsResource(new StringAsset( - "dummy.name=quarkus\ndummy.numbers=1,2,3,4\ndummy.boolWithDefault=true\ndummy.optionalInt=100"), + "dummy.name=quarkus\ndummy.numbers=1,2,3,4\ndummy.bool-with-default=true\ndummy.optional-int=100"), "application.properties")); @Inject diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalInterfaceConfigPropertiesTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalInterfaceConfigPropertiesTest.java index bb2ed370d2f71..ba31954273c74 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalInterfaceConfigPropertiesTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/TypicalInterfaceConfigPropertiesTest.java @@ -28,7 +28,7 @@ public class TypicalInterfaceConfigPropertiesTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(DummyBean.class, DummyProperties.class) .addAsResource(new StringAsset( - "dummy.name=quarkus\ndummy.numbers=1,2,3,4\ndummy.boolWithDefault=true\ndummy.optionalInt=100"), + "dummy.name=quarkus\ndummy.numbers=1,2,3,4\ndummy.boolWD=true\ndummy.optional-int=100"), "application.properties")); @Inject @@ -87,7 +87,7 @@ public interface DummyProperties { @ConfigProperty(name = "name") String getFirstName(); - @ConfigProperty(name = "boolWithDefault", defaultValue = "false") + @ConfigProperty(name = "boolWD", defaultValue = "false") boolean isBoolWithDef(); @ConfigProperty(name = "doubleWithDefault", defaultValue = "1.0") diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/VerbatimConfigPropertiesTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/VerbatimConfigPropertiesTest.java new file mode 100644 index 0000000000000..1fb2e2586ba74 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/VerbatimConfigPropertiesTest.java @@ -0,0 +1,72 @@ +package io.quarkus.arc.test.configproperties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.config.ConfigProperties; +import io.quarkus.test.QuarkusUnitTest; + +public class VerbatimConfigPropertiesTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(DummyBean.class, DummyProperties.class) + .addAsResource(new StringAsset( + "dummy.fooBar=quarkus1\ndummy.foo=quarkus2\ndummy.unused=whatever"), + "application.properties")); + + @Inject + DummyBean dummyBean; + + @Test + public void testConfiguredValues() { + assertEquals("quarkus1", dummyBean.getFooBar()); + assertEquals("quarkus2", dummyBean.getFoo()); + } + + @Singleton + public static class DummyBean { + @Inject + DummyProperties dummyProperties; + + String getFoo() { + return dummyProperties.getFoo(); + } + + String getFooBar() { + return dummyProperties.getFooBar(); + } + } + + @ConfigProperties(prefix = "dummy", namingStrategy = ConfigProperties.NamingStrategy.VERBATIM) + public static class DummyProperties { + + public String foo; + public String fooBar; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getFooBar() { + return fooBar; + } + + public void setFooBar(String fooBar) { + this.fooBar = fooBar; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/VerbatimInterfaceConfigPropertiesTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/VerbatimInterfaceConfigPropertiesTest.java new file mode 100644 index 0000000000000..1de9761ecbea9 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/configproperties/VerbatimInterfaceConfigPropertiesTest.java @@ -0,0 +1,74 @@ +package io.quarkus.arc.test.configproperties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.config.ConfigProperties; +import io.quarkus.test.QuarkusUnitTest; + +public class VerbatimInterfaceConfigPropertiesTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(DummyBean.class, DummyProperties.class) + .addAsResource(new StringAsset( + "dummy.name=quarkus\ndummy.bool-with-default=true\ndummy.optional-int=100\ndummy.numbers=1,2,3,4"), + "application.properties")); + + @Inject + DummyBean dummyBean; + + @Test + public void testConfiguredValues() { + assertEquals(Arrays.asList(1, 2, 3, 4), dummyBean.numbers()); + assertTrue(dummyBean.boolWithDefault()); + assertTrue(dummyBean.getOptionalInt().isPresent()); + assertEquals(100, dummyBean.getOptionalInt().get()); + } + + @Singleton + public static class DummyBean { + + @Inject + DummyProperties dummyProperties; + + Collection numbers() { + return dummyProperties.numbersWithoutDefault(); + } + + boolean boolWithDefault() { + return dummyProperties.boolWithDefault(); + } + + Optional getOptionalInt() { + return dummyProperties.optionalInt(); + } + } + + @ConfigProperties(prefix = "dummy", namingStrategy = ConfigProperties.NamingStrategy.KEBAB_CASE) + public interface DummyProperties { + + @ConfigProperty(defaultValue = "false") + boolean boolWithDefault(); + + @ConfigProperty(name = "numbers") + Collection numbersWithoutDefault(); + + Optional optionalInt(); + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/observer/ObserverTransformerTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/observer/ObserverTransformerTest.java new file mode 100644 index 0000000000000..5e6d57a1ca69c --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/observer/ObserverTransformerTest.java @@ -0,0 +1,122 @@ +package io.quarkus.arc.test.observer; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Qualifier; +import javax.inject.Singleton; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.google.inject.Inject; + +import io.quarkus.arc.deployment.ObserverTransformerBuildItem; +import io.quarkus.arc.processor.ObserverTransformer; +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; + +public class ObserverTransformerTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyObserver.class, AlphaQualifier.class, BravoQualifier.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + + @Override + public void execute(BuildContext context) { + context.produce(new ObserverTransformerBuildItem(new ObserverTransformer() { + + @Override + public boolean appliesTo(Type observedType, Set qualifiers) { + return observedType.name().equals(DotName.createSimple(MyEvent.class.getName())); + } + + @Override + public void transform(TransformationContext context) { + if (context.getMethod().name().equals("")) { + context.transform().removeAll().done(); + } + } + + })); + } + }).produces(ObserverTransformerBuildItem.class).build(); + } + }; + } + + @BravoQualifier + @Inject + Event event; + + @Test + public void testTransformation() { + MyEvent myEvent = new MyEvent(); + event.fire(myEvent); + // MyObserver.onMyEventRemoveQualifiers() would not match without transformation + assertEquals(1, myEvent.log.size()); + assertEquals("onMyEventRemoveQualifiers", myEvent.log.get(0)); + } + + @Singleton + static class MyObserver { + + void onMyEventRemoveQualifiers(@Observes @BravoQualifier MyEvent event) { + event.log.add("onMyEventRemoveQualifiers"); + } + + } + + @Qualifier + @Inherited + @Target({ TYPE, METHOD, FIELD, PARAMETER }) + @Retention(RUNTIME) + public @interface AlphaQualifier { + + } + + @Qualifier + @Inherited + @Target({ TYPE, METHOD, FIELD, PARAMETER }) + @Retention(RUNTIME) + public @interface BravoQualifier { + + } + + static class MyEvent { + + final List log = new ArrayList<>(); + + } + +} diff --git a/extensions/arc/runtime/pom.xml b/extensions/arc/runtime/pom.xml index 8f0d2a3bc607d..bd99441c3cb12 100644 --- a/extensions/arc/runtime/pom.xml +++ b/extensions/arc/runtime/pom.xml @@ -1,52 +1,52 @@ - - quarkus-arc-parent - io.quarkus - 999-SNAPSHOT - ../ - - 4.0.0 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + quarkus-arc-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 - quarkus-arc - Quarkus - ArC - Runtime - Build time CDI dependency injection + quarkus-arc + Quarkus - ArC - Runtime + Build time CDI dependency injection - - - io.quarkus.arc - arc - - - io.quarkus - quarkus-core - - - org.eclipse.microprofile.context-propagation - microprofile-context-propagation-api - - + + + io.quarkus.arc + arc + + + io.quarkus + quarkus-core + + + org.eclipse.microprofile.context-propagation + microprofile-context-propagation-api + + - - - - io.quarkus - quarkus-bootstrap-maven-plugin - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/config/ConfigProperties.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/config/ConfigProperties.java index aa3bd038539da..a7662f36686b2 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/config/ConfigProperties.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/config/ConfigProperties.java @@ -6,6 +6,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import io.quarkus.runtime.util.StringUtil; + /** * Allow configuration properties with a common prefix to be grouped into a single class */ @@ -19,4 +21,54 @@ * If the default is used, the class name will be used to determine the proper prefix */ String prefix() default UNSET_PREFIX; + + /** + * The naming strategy to use for the corresponding property. This only matters for fields or method names that contain + * both lower case and upper case characters. + * + * {@code NamingStrategy.VERBATIM} means that whatever the name of the field / method is, that will be the name of the + * property. + * {@code NamingStrategy.KEBAB_CASE} means that the name of property is derived by replacing case changes with a dash. + * For a example: + * + * /** + * + *

+     * @ConfigProperties(prefix="whatever")
+     * public class SomeConfig {
+     *   public fooBar;
+     * }
+     * 
+ * + * Then to set the {@code fooBar} field, the corresponding property would be {@code whatever.fooBar}. + * If {@code namingStrategy=NamingStrategy.KEBAB_CASE} were being used, then the corresponding property would be + * {@code whatever.foo-bar} + * + * When this field is not set, then the default strategy will be determined by the value of + * quarkus.arc.config-properties-default-naming-strategy + */ + NamingStrategy namingStrategy() default NamingStrategy.FROM_CONFIG; + + enum NamingStrategy { + FROM_CONFIG { + @Override + public String getName(String name) { + throw new IllegalStateException("The naming strategy needs to substituted with the configured naming strategy"); + } + }, + VERBATIM { + @Override + public String getName(String name) { + return name; + } + }, + KEBAB_CASE { + @Override + public String getName(String name) { + return StringUtil.hyphenate(name); + } + }; + + public abstract String getName(String name); + } } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanInvoker.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanInvoker.java new file mode 100644 index 0000000000000..d0a0741e3e436 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanInvoker.java @@ -0,0 +1,29 @@ +package io.quarkus.arc.runtime; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ManagedContext; + +/** + * Invokes a business method of a bean. The request context is activated if necessary. + * + * @param + */ +public interface BeanInvoker { + + default void invoke(T param) { + ManagedContext requestContext = Arc.container().requestContext(); + if (requestContext.isActive()) { + invokeBean(param); + } else { + try { + requestContext.activate(); + invokeBean(param); + } finally { + requestContext.terminate(); + } + } + } + + void invokeBean(T param); + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java index fa6b238652058..3f652d1dade3f 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java @@ -8,8 +8,8 @@ import javax.enterprise.inject.spi.InjectionPoint; import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import io.quarkus.arc.BeanCreator; import io.quarkus.arc.impl.InjectionPointProvider; @@ -58,7 +58,7 @@ public Object create(CreationalContext creationalContext, Map> properties) { - Config config = ConfigProviderResolver.instance().getConfig(); + Config config = ConfigProvider.getConfig(); ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { cl = ConfigRecorder.class.getClassLoader(); diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/QuarkusConfigProducer.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/QuarkusConfigProducer.java deleted file mode 100644 index 548fb96ce7f05..0000000000000 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/QuarkusConfigProducer.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.quarkus.arc.runtime; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.context.Dependent; -import javax.enterprise.inject.Produces; -import javax.enterprise.inject.spi.InjectionPoint; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; - -import io.smallrye.config.inject.ConfigProducerUtil; - -/** - * This class is the same as io.smallrye.config.inject.ConfigProducer - * but uses the proper Quarkus way of obtaining org.eclipse.microprofile.config.Config - */ -@ApplicationScoped -public class QuarkusConfigProducer { - - @Produces - Config getConfig(InjectionPoint injectionPoint) { - return ConfigProviderResolver.instance().getConfig(); - } - - @Dependent - @Produces - @ConfigProperty - String produceStringConfigProperty(InjectionPoint ip) { - return ConfigProducerUtil.getValue(ip, String.class, getConfig(ip)); - } - - @Dependent - @Produces - @ConfigProperty - Long getLongValue(InjectionPoint ip) { - return ConfigProducerUtil.getValue(ip, Long.class, getConfig(ip)); - } - - @Dependent - @Produces - @ConfigProperty - Integer getIntegerValue(InjectionPoint ip) { - return ConfigProducerUtil.getValue(ip, Integer.class, getConfig(ip)); - } - - @Dependent - @Produces - @ConfigProperty - Float produceFloatConfigProperty(InjectionPoint ip) { - return ConfigProducerUtil.getValue(ip, Float.class, getConfig(ip)); - } - - @Dependent - @Produces - @ConfigProperty - Double produceDoubleConfigProperty(InjectionPoint ip) { - return ConfigProducerUtil.getValue(ip, Double.class, getConfig(ip)); - } - - @Dependent - @Produces - @ConfigProperty - Boolean produceBooleanConfigProperty(InjectionPoint ip) { - return ConfigProducerUtil.getValue(ip, Boolean.class, getConfig(ip)); - } - - @Dependent - @Produces - @ConfigProperty - Optional produceOptionalConfigValue(InjectionPoint injectionPoint) { - return ConfigProducerUtil.optionalConfigValue(injectionPoint, getConfig(injectionPoint)); - } - - @Dependent - @Produces - @ConfigProperty - Set producesSetConfigPropery(InjectionPoint ip) { - return ConfigProducerUtil.collectionConfigProperty(ip, getConfig(ip), new HashSet<>()); - } - - @Dependent - @Produces - @ConfigProperty - List producesListConfigPropery(InjectionPoint ip) { - return ConfigProducerUtil.collectionConfigProperty(ip, getConfig(ip), new ArrayList()); - } - -} diff --git a/extensions/artemis-core/deployment/pom.xml b/extensions/artemis-core/deployment/pom.xml index b5212a74ca088..d25a4fff99334 100644 --- a/extensions/artemis-core/deployment/pom.xml +++ b/extensions/artemis-core/deployment/pom.xml @@ -28,6 +28,11 @@ quarkus-netty-deployment + + io.quarkus + quarkus-smallrye-health-spi + + io.quarkus quarkus-artemis-core diff --git a/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisBuildTimeConfig.java b/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisBuildTimeConfig.java new file mode 100644 index 0000000000000..767ff493eff92 --- /dev/null +++ b/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisBuildTimeConfig.java @@ -0,0 +1,15 @@ +package io.quarkus.artemis.core.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "artemis", phase = ConfigPhase.BUILD_TIME) +public class ArtemisBuildTimeConfig { + + /** + * Whether or not an health check is published in case the smallrye-health extension is present + */ + @ConfigItem(name = "health.enabled", defaultValue = "true") + public boolean healthEnabled; +} diff --git a/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisCoreConfiguredBuildItem.java b/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisCoreConfiguredBuildItem.java new file mode 100644 index 0000000000000..164dcf04a187e --- /dev/null +++ b/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisCoreConfiguredBuildItem.java @@ -0,0 +1,9 @@ +package io.quarkus.artemis.core.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Marker build item indicating that Artemis Core is configured + */ +public final class ArtemisCoreConfiguredBuildItem extends SimpleBuildItem { +} diff --git a/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisCoreProcessor.java b/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisCoreProcessor.java index a3cd731d87130..0d6fdcd4c7d24 100644 --- a/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisCoreProcessor.java +++ b/extensions/artemis-core/deployment/src/main/java/io/quarkus/artemis/core/deployment/ArtemisCoreProcessor.java @@ -27,6 +27,7 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; public class ArtemisCoreProcessor { @@ -78,6 +79,17 @@ void build(CombinedIndexBuildItem indexBuildItem, } } + @BuildStep + HealthBuildItem health(ArtemisBuildTimeConfig buildConfig, Optional artemisJms) { + if (artemisJms.isPresent()) { + return null; + } + + return new HealthBuildItem( + "io.quarkus.artemis.core.runtime.health.ServerLocatorHealthCheck", + buildConfig.healthEnabled, "artemis"); + } + @BuildStep void load(BuildProducer additionalBean, BuildProducer feature, Optional artemisJms) { @@ -91,12 +103,13 @@ void load(BuildProducer additionalBean, BuildProducer artemisJms) { if (artemisJms.isPresent()) { - return; + return null; } recorder.setConfig(runtimeConfig, beanContainer.getValue()); + return new ArtemisCoreConfiguredBuildItem(); } } diff --git a/extensions/artemis-core/runtime/pom.xml b/extensions/artemis-core/runtime/pom.xml index 2471ba5700fe6..2ce771b64827b 100644 --- a/extensions/artemis-core/runtime/pom.xml +++ b/extensions/artemis-core/runtime/pom.xml @@ -24,15 +24,34 @@ quarkus-arc + + io.quarkus + quarkus-jsonp + + io.quarkus quarkus-netty + + io.quarkus + quarkus-smallrye-health + true + + org.apache.activemq artemis-core-client + + org.apache.geronimo.specs + geronimo-json_1.0_spec + + + org.apache.johnzon + johnzon-core + commons-logging commons-logging diff --git a/extensions/artemis-core/runtime/src/main/java/io/quarkus/artemis/core/runtime/ArtemisCoreProducer.java b/extensions/artemis-core/runtime/src/main/java/io/quarkus/artemis/core/runtime/ArtemisCoreProducer.java index 44fa7a1d580d6..6ee05aeb1d237 100644 --- a/extensions/artemis-core/runtime/src/main/java/io/quarkus/artemis/core/runtime/ArtemisCoreProducer.java +++ b/extensions/artemis-core/runtime/src/main/java/io/quarkus/artemis/core/runtime/ArtemisCoreProducer.java @@ -6,13 +6,17 @@ import org.apache.activemq.artemis.api.core.client.ActiveMQClient; import org.apache.activemq.artemis.api.core.client.ServerLocator; +import io.quarkus.arc.DefaultBean; + @ApplicationScoped public class ArtemisCoreProducer { private ArtemisRuntimeConfig config; @Produces - public ServerLocator produceServerLocator() throws Exception { + @ApplicationScoped + @DefaultBean + public ServerLocator serverLocator() throws Exception { return ActiveMQClient.createServerLocator(config.url); } diff --git a/extensions/artemis-core/runtime/src/main/java/io/quarkus/artemis/core/runtime/health/ServerLocatorHealthCheck.java b/extensions/artemis-core/runtime/src/main/java/io/quarkus/artemis/core/runtime/health/ServerLocatorHealthCheck.java new file mode 100644 index 0000000000000..9eed5dac5337d --- /dev/null +++ b/extensions/artemis-core/runtime/src/main/java/io/quarkus/artemis/core/runtime/health/ServerLocatorHealthCheck.java @@ -0,0 +1,30 @@ +package io.quarkus.artemis.core.runtime.health; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; +import org.apache.activemq.artemis.api.core.client.ServerLocator; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; + +@Readiness +@ApplicationScoped +public class ServerLocatorHealthCheck implements HealthCheck { + + @Inject + ServerLocator serverLocator; + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named("Artemis Core health check"); + try (ClientSessionFactory factory = serverLocator.createSessionFactory()) { + builder.up(); + } catch (Exception e) { + builder.down(); + } + return builder.build(); + } +} diff --git a/extensions/artemis-jms/deployment/src/main/java/io/quarkus/artemis/jms/deployment/ArtemisJmsConfiguredBuildItem.java b/extensions/artemis-jms/deployment/src/main/java/io/quarkus/artemis/jms/deployment/ArtemisJmsConfiguredBuildItem.java new file mode 100644 index 0000000000000..288693fc7ee88 --- /dev/null +++ b/extensions/artemis-jms/deployment/src/main/java/io/quarkus/artemis/jms/deployment/ArtemisJmsConfiguredBuildItem.java @@ -0,0 +1,9 @@ +package io.quarkus.artemis.jms.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Marker build item indicating that Artemis JMS is configured + */ +public final class ArtemisJmsConfiguredBuildItem extends SimpleBuildItem { +} diff --git a/extensions/artemis-jms/deployment/src/main/java/io/quarkus/artemis/jms/deployment/ArtemisJmsProcessor.java b/extensions/artemis-jms/deployment/src/main/java/io/quarkus/artemis/jms/deployment/ArtemisJmsProcessor.java index 9890871e44af2..5108472cf9043 100644 --- a/extensions/artemis-jms/deployment/src/main/java/io/quarkus/artemis/jms/deployment/ArtemisJmsProcessor.java +++ b/extensions/artemis-jms/deployment/src/main/java/io/quarkus/artemis/jms/deployment/ArtemisJmsProcessor.java @@ -2,6 +2,7 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.artemis.core.deployment.ArtemisBuildTimeConfig; import io.quarkus.artemis.core.deployment.ArtemisJmsBuildItem; import io.quarkus.artemis.core.runtime.ArtemisRuntimeConfig; import io.quarkus.artemis.jms.runtime.ArtemisJmsProducer; @@ -11,6 +12,7 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; public class ArtemisJmsProcessor { @@ -23,11 +25,19 @@ void load(BuildProducer additionalBean, BuildProducerquarkus-artemis-core + + io.quarkus + quarkus-smallrye-health + true + + org.apache.activemq artemis-jms-client diff --git a/extensions/artemis-jms/runtime/src/main/java/io/quarkus/artemis/jms/runtime/ArtemisJmsProducer.java b/extensions/artemis-jms/runtime/src/main/java/io/quarkus/artemis/jms/runtime/ArtemisJmsProducer.java index a02a085871013..7e4748c7a170b 100644 --- a/extensions/artemis-jms/runtime/src/main/java/io/quarkus/artemis/jms/runtime/ArtemisJmsProducer.java +++ b/extensions/artemis-jms/runtime/src/main/java/io/quarkus/artemis/jms/runtime/ArtemisJmsProducer.java @@ -6,6 +6,7 @@ import org.apache.activemq.artemis.jms.client.ActiveMQJMSConnectionFactory; +import io.quarkus.arc.DefaultBean; import io.quarkus.artemis.core.runtime.ArtemisRuntimeConfig; @ApplicationScoped @@ -14,7 +15,9 @@ public class ArtemisJmsProducer { private ArtemisRuntimeConfig config; @Produces - public ConnectionFactory producesConnectionFactory() { + @ApplicationScoped + @DefaultBean + public ConnectionFactory connectionFactory() { return new ActiveMQJMSConnectionFactory(config.url, config.username.orElse(null), config.password.orElse(null)); } diff --git a/extensions/artemis-jms/runtime/src/main/java/io/quarkus/artemis/jms/runtime/health/ConnectionFactoryHealthCheck.java b/extensions/artemis-jms/runtime/src/main/java/io/quarkus/artemis/jms/runtime/health/ConnectionFactoryHealthCheck.java new file mode 100644 index 0000000000000..c171de77079a8 --- /dev/null +++ b/extensions/artemis-jms/runtime/src/main/java/io/quarkus/artemis/jms/runtime/health/ConnectionFactoryHealthCheck.java @@ -0,0 +1,30 @@ +package io.quarkus.artemis.jms.runtime.health; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.jms.Connection; +import javax.jms.ConnectionFactory; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; + +@Readiness +@ApplicationScoped +public class ConnectionFactoryHealthCheck implements HealthCheck { + + @Inject + ConnectionFactory connectionFactory; + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named("Artemis JMS health check"); + try (Connection connection = connectionFactory.createConnection()) { + builder.up(); + } catch (Exception e) { + builder.down(); + } + return builder.build(); + } +} diff --git a/extensions/azure-functions-http/maven-archetype/pom.xml b/extensions/azure-functions-http/maven-archetype/pom.xml index b2f009ed69dd7..29634eb650eb5 100644 --- a/extensions/azure-functions-http/maven-archetype/pom.xml +++ b/extensions/azure-functions-http/maven-archetype/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-azure-functions-http-archetype - Quarkus - HTTP Azure Functions Archetype + Quarkus - HTTP Azure Functions - Archetype maven-archetype @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/extensions/config-yaml/deployment/pom.xml b/extensions/config-yaml/deployment/pom.xml new file mode 100644 index 0000000000000..727470f3de722 --- /dev/null +++ b/extensions/config-yaml/deployment/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + io.quarkus + quarkus-config-yaml-parent + 999-SNAPSHOT + ../ + + + quarkus-config-yaml-deployment + Quarkus - Configuration - YAML - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-config-yaml + + + + io.quarkus + quarkus-resteasy-deployment + test + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/config-yaml/deployment/src/main/java/io/quarkus/config/yaml/deployment/ConfigYamlProcessor.java b/extensions/config-yaml/deployment/src/main/java/io/quarkus/config/yaml/deployment/ConfigYamlProcessor.java new file mode 100644 index 0000000000000..025d1539976ce --- /dev/null +++ b/extensions/config-yaml/deployment/src/main/java/io/quarkus/config/yaml/deployment/ConfigYamlProcessor.java @@ -0,0 +1,19 @@ +package io.quarkus.config.yaml.deployment; + +import io.quarkus.config.yaml.runtime.ApplicationYamlProvider; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; + +public final class ConfigYamlProcessor { + + @BuildStep + public FeatureBuildItem feature() { + return new FeatureBuildItem(FeatureBuildItem.CONFIG_YAML); + } + + @BuildStep + HotDeploymentWatchedFileBuildItem watchYamlConfig() { + return new HotDeploymentWatchedFileBuildItem(ApplicationYamlProvider.APPLICATION_YAML); + } +} diff --git a/extensions/config-yaml/deployment/src/test/java/io/quarkus/config/yaml/deployment/ApplicationYamlHotDeploymentTest.java b/extensions/config-yaml/deployment/src/test/java/io/quarkus/config/yaml/deployment/ApplicationYamlHotDeploymentTest.java new file mode 100644 index 0000000000000..4f86ed8fd5a1b --- /dev/null +++ b/extensions/config-yaml/deployment/src/test/java/io/quarkus/config/yaml/deployment/ApplicationYamlHotDeploymentTest.java @@ -0,0 +1,34 @@ +package io.quarkus.config.yaml.deployment; + +import static io.quarkus.config.yaml.runtime.ApplicationYamlProvider.APPLICATION_YAML; +import static org.hamcrest.Matchers.is; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class ApplicationYamlHotDeploymentTest { + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(APPLICATION_YAML) + .addClass(FooResource.class)); + + @Test + public void testConfigReload() { + RestAssured.when().get("/foo").then() + .statusCode(200) + .body(is("AAAA")); + + test.modifyResourceFile(APPLICATION_YAML, s -> s.replace("AAAA", "BBBB")); + + RestAssured.when().get("/foo").then() + .statusCode(200) + .body(is("BBBB")); + } +} diff --git a/extensions/config-yaml/deployment/src/test/java/io/quarkus/config/yaml/deployment/FooResource.java b/extensions/config-yaml/deployment/src/test/java/io/quarkus/config/yaml/deployment/FooResource.java new file mode 100644 index 0000000000000..d07c09d18b7e9 --- /dev/null +++ b/extensions/config-yaml/deployment/src/test/java/io/quarkus/config/yaml/deployment/FooResource.java @@ -0,0 +1,22 @@ +package io.quarkus.config.yaml.deployment; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@Path("/") +public class FooResource { + + @ConfigProperty(name = "foo.bar") + String fooBar; + + @Path("foo") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String get() { + return fooBar; + } +} diff --git a/extensions/config-yaml/deployment/src/test/resources/application.yaml b/extensions/config-yaml/deployment/src/test/resources/application.yaml new file mode 100644 index 0000000000000..9ef11adb25415 --- /dev/null +++ b/extensions/config-yaml/deployment/src/test/resources/application.yaml @@ -0,0 +1,2 @@ +foo: + bar: AAAA \ No newline at end of file diff --git a/extensions/config-yaml/pom.xml b/extensions/config-yaml/pom.xml new file mode 100644 index 0000000000000..f2e2c099a56b7 --- /dev/null +++ b/extensions/config-yaml/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-config-yaml-parent + Quarkus - Configuration - YAML + pom + + runtime + deployment + + + diff --git a/extensions/config-yaml/runtime/config/application.yaml b/extensions/config-yaml/runtime/config/application.yaml new file mode 100644 index 0000000000000..2697ced792665 --- /dev/null +++ b/extensions/config-yaml/runtime/config/application.yaml @@ -0,0 +1,2 @@ +file: + system: true \ No newline at end of file diff --git a/extensions/config-yaml/runtime/pom.xml b/extensions/config-yaml/runtime/pom.xml new file mode 100644 index 0000000000000..4b6fcb3e2acbe --- /dev/null +++ b/extensions/config-yaml/runtime/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + io.quarkus + quarkus-config-yaml-parent + 999-SNAPSHOT + + + quarkus-config-yaml + Quarkus - Configuration - YAML - Runtime + Use YAML to configure your Quarkus application + + + + io.smallrye.config + smallrye-config-source-yaml + + + io.quarkus + quarkus-core + + + org.eclipse.microprofile.config + microprofile-config-api + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/config-yaml/runtime/src/main/java/io/quarkus/config/yaml/runtime/ApplicationYamlProvider.java b/extensions/config-yaml/runtime/src/main/java/io/quarkus/config/yaml/runtime/ApplicationYamlProvider.java new file mode 100644 index 0000000000000..3605f0fe2f8ba --- /dev/null +++ b/extensions/config-yaml/runtime/src/main/java/io/quarkus/config/yaml/runtime/ApplicationYamlProvider.java @@ -0,0 +1,65 @@ +package io.quarkus.config.yaml.runtime; + +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; + +import io.smallrye.config.source.yaml.YamlConfigSource; + +/** + * + */ +public final class ApplicationYamlProvider implements ConfigSourceProvider { + + public static final String APPLICATION_YAML = "application.yaml"; + + @Override + public Iterable getConfigSources(final ClassLoader forClassLoader) { + List sources = Collections.emptyList(); + // mirror the in-JAR application.properties + try { + InputStream str = forClassLoader.getResourceAsStream(APPLICATION_YAML); + if (str != null) { + try (Closeable c = str) { + YamlConfigSource configSource = new YamlConfigSource(APPLICATION_YAML, str, 254); + assert sources.isEmpty(); + sources = Collections.singletonList(configSource); + } + } + } catch (IOException e) { + // configuration problem should be thrown + throw new IOError(e); + } + // mirror the on-filesystem application.properties + final Path path = Paths.get("config", APPLICATION_YAML); + if (Files.exists(path)) { + try (InputStream str = Files.newInputStream(path)) { + YamlConfigSource configSource = new YamlConfigSource(APPLICATION_YAML, str, 264); + if (sources.isEmpty()) { + sources = Collections.singletonList(configSource); + } else { + // todo: sources = List.of(sources.get(0), configSource); + sources = Arrays.asList(sources.get(0), configSource); + } + } catch (NoSuchFileException | FileNotFoundException e) { + // skip (race) + } catch (IOException e) { + // configuration problem should be thrown + throw new IOError(e); + } + } + return sources; + } +} diff --git a/extensions/config-yaml/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/config-yaml/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..be50cd6e20284 --- /dev/null +++ b/extensions/config-yaml/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +name: "YAML Configuration" +metadata: + keywords: + - "config" + - "configuration" + - "yaml" + categories: + - "core" + status: "stable" + guide: "https://quarkus.io/guides/config#yaml" diff --git a/extensions/config-yaml/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider b/extensions/config-yaml/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider new file mode 100644 index 0000000000000..e85b2e9dda1c0 --- /dev/null +++ b/extensions/config-yaml/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider @@ -0,0 +1 @@ +io.quarkus.config.yaml.runtime.ApplicationYamlProvider diff --git a/extensions/config-yaml/runtime/src/test/java/io/quarkus/config/yaml/runtime/ApplicationYamlTest.java b/extensions/config-yaml/runtime/src/test/java/io/quarkus/config/yaml/runtime/ApplicationYamlTest.java new file mode 100644 index 0000000000000..9425ea39c2af3 --- /dev/null +++ b/extensions/config-yaml/runtime/src/test/java/io/quarkus/config/yaml/runtime/ApplicationYamlTest.java @@ -0,0 +1,52 @@ +package io.quarkus.config.yaml.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.quarkus.runtime.configuration.QuarkusConfigFactory; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +/** + * Test the YAML config provider (plan JUnit). We aren't re-testing the whole config source + * (that's done in SmallRye Config) but we do make sure that both the file system and in-JAR + * properties are being picked up. + */ +public class ApplicationYamlTest { + + static volatile SmallRyeConfig config; + + @BeforeAll + public static void doBefore() { + SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder(); + builder.addDefaultSources().addDiscoveredConverters().addDiscoveredSources(); + QuarkusConfigFactory.setConfig(config = builder.build()); + Config conf = ConfigProvider.getConfig(); + if (conf != config) { + ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + cpr.releaseConfig(conf); + ConfigProvider.getConfig(); + } + System.out.println(System.getProperty("user.dir")); + } + + @Test + public void testBasicApplicationYaml() { + assertEquals("something", ConfigProvider.getConfig().getValue("foo.bar", String.class)); + assertTrue(ConfigProvider.getConfig().getValue("file.system", Boolean.class).booleanValue()); + } + + @AfterAll + public static void doAfter() { + ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + cpr.releaseConfig(config); + config = null; + } +} diff --git a/extensions/config-yaml/runtime/src/test/resources/application.yaml b/extensions/config-yaml/runtime/src/test/resources/application.yaml new file mode 100644 index 0000000000000..5655415edd8aa --- /dev/null +++ b/extensions/config-yaml/runtime/src/test/resources/application.yaml @@ -0,0 +1,2 @@ +foo: + bar: something diff --git a/extensions/elytron-security-common/deployment/pom.xml b/extensions/elytron-security-common/deployment/pom.xml new file mode 100644 index 0000000000000..9cb7588f3adf7 --- /dev/null +++ b/extensions/elytron-security-common/deployment/pom.xml @@ -0,0 +1,39 @@ + + + + quarkus-elytron-security-common-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-elytron-security-common-deployment + Quarkus - Elytron Security - Common - Deployment + + + + io.quarkus + quarkus-core-deployment + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/elytron-security-common/deployment/src/main/java/io/quarkus/elytron/security/common/deployment/DummyForJavadoc.java b/extensions/elytron-security-common/deployment/src/main/java/io/quarkus/elytron/security/common/deployment/DummyForJavadoc.java new file mode 100644 index 0000000000000..0eb1f2036ad72 --- /dev/null +++ b/extensions/elytron-security-common/deployment/src/main/java/io/quarkus/elytron/security/common/deployment/DummyForJavadoc.java @@ -0,0 +1,8 @@ +package io.quarkus.elytron.security.common.deployment; + +/** + * Quick workaround to have at least one public class and generate a Javadoc jar. + */ +public class DummyForJavadoc { + +} diff --git a/extensions/elytron-security-common/pom.xml b/extensions/elytron-security-common/pom.xml new file mode 100644 index 0000000000000..f8520149f08e6 --- /dev/null +++ b/extensions/elytron-security-common/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-elytron-security-common-parent + Quarkus - Elytron Security - Common + pom + + deployment + runtime + + diff --git a/extensions/elytron-security-common/runtime/pom.xml b/extensions/elytron-security-common/runtime/pom.xml new file mode 100644 index 0000000000000..dff337ab6810f --- /dev/null +++ b/extensions/elytron-security-common/runtime/pom.xml @@ -0,0 +1,50 @@ + + + + quarkus-elytron-security-common-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-elytron-security-common + Quarkus - Elytron Security - Common - Runtime + + + io.quarkus + quarkus-core + + + com.oracle.substratevm + svm + + + org.wildfly.security + wildfly-elytron-credential + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/DummyForJavadoc.java b/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/DummyForJavadoc.java new file mode 100644 index 0000000000000..d9752130350ab --- /dev/null +++ b/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/DummyForJavadoc.java @@ -0,0 +1,8 @@ +package io.quarkus.elytron.security.common.runtime.graal; + +/** + * Quick workaround to have at least one public class and generate a Javadoc jar. + */ +public class DummyForJavadoc { + +} diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_BCryptPassword.java b/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_BCryptPassword.java similarity index 100% rename from extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_BCryptPassword.java rename to extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_BCryptPassword.java diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_BSDUnixDESCryptPassword.java b/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_BSDUnixDESCryptPassword.java similarity index 100% rename from extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_BSDUnixDESCryptPassword.java rename to extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_BSDUnixDESCryptPassword.java diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_ClearPassword.java b/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_ClearPassword.java similarity index 100% rename from extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_ClearPassword.java rename to extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_ClearPassword.java diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_RawClearPassword.java b/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_RawClearPassword.java similarity index 100% rename from extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_password_interfaces_RawClearPassword.java rename to extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_password_interfaces_RawClearPassword.java diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_x500_util_X500PrincipalUtil.java b/extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_x500_util_X500PrincipalUtil.java similarity index 100% rename from extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/graal/Target_org_wildfly_security_x500_util_X500PrincipalUtil.java rename to extensions/elytron-security-common/runtime/src/main/java/io/quarkus/elytron/security/common/runtime/graal/Target_org_wildfly_security_x500_util_X500PrincipalUtil.java diff --git a/extensions/elytron-security-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/elytron-security-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..97696455e3850 --- /dev/null +++ b/extensions/elytron-security-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +--- +name: "Elytron Security Common" +metadata: + keywords: + - "security" + categories: + - "security" + stable: "true" + unlisted: "true" diff --git a/extensions/elytron-security-jdbc/deployment/src/main/java/io/quarkus/elytron/security/jdbc/deployment/ElytronSecurityJdbcProcessor.java b/extensions/elytron-security-jdbc/deployment/src/main/java/io/quarkus/elytron/security/jdbc/deployment/ElytronSecurityJdbcProcessor.java index 4237f0289cb66..20a8fe922bae2 100644 --- a/extensions/elytron-security-jdbc/deployment/src/main/java/io/quarkus/elytron/security/jdbc/deployment/ElytronSecurityJdbcProcessor.java +++ b/extensions/elytron-security-jdbc/deployment/src/main/java/io/quarkus/elytron/security/jdbc/deployment/ElytronSecurityJdbcProcessor.java @@ -46,7 +46,7 @@ FeatureBuildItem feature() { * @throws Exception - on any failure */ @BuildStep - @Record(ExecutionTime.STATIC_INIT) + @Record(ExecutionTime.RUNTIME_INIT) void configureJdbcRealmAuthConfig(JdbcRecorder recorder, BuildProducer securityRealm, BeanContainerBuildItem beanContainerBuildItem, //we need this to make sure ArC is initialized diff --git a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/JdbcRecorder.java b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/JdbcRecorder.java index 8baf6209befb8..1949f76614804 100644 --- a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/JdbcRecorder.java +++ b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/JdbcRecorder.java @@ -19,6 +19,8 @@ @Recorder public class JdbcRecorder { + private static final Provider[] PROVIDERS = new Provider[] { new WildFlyElytronProvider() }; + /** * Create a runtime value for a {@linkplain JdbcSecurityRealm} * @@ -29,7 +31,7 @@ public RuntimeValue createRealm(JdbcSecurityRealmConfig config) { Supplier providers = new Supplier() { @Override public Provider[] get() { - return new Provider[] { new WildFlyElytronProvider() }; + return PROVIDERS; } }; JdbcSecurityRealmBuilder builder = JdbcSecurityRealm.builder().setProviders(providers); diff --git a/extensions/elytron-security-ldap/deployment/pom.xml b/extensions/elytron-security-ldap/deployment/pom.xml new file mode 100644 index 0000000000000..3cf64668dd879 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + io.quarkus + quarkus-elytron-security-ldap-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-elytron-security-ldap-deployment + Quarkus - Elytron Security LDAP - Deployment + + + + io.quarkus + quarkus-elytron-security-ldap + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-elytron-security-deployment + + + + io.quarkus + quarkus-undertow + test + + + io.quarkus + quarkus-undertow-deployment + test + + + io.quarkus + quarkus-resteasy-deployment + test + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-ldap + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java b/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java new file mode 100644 index 0000000000000..56bf205517f67 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java @@ -0,0 +1,73 @@ +package io.quarkus.elytron.security.ldap.deployment; + +import org.wildfly.security.auth.server.SecurityRealm; + +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CapabilityBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.JniBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.elytron.security.deployment.ElytronPasswordMarkerBuildItem; +import io.quarkus.elytron.security.deployment.SecurityRealmBuildItem; +import io.quarkus.elytron.security.ldap.LdapRecorder; +import io.quarkus.elytron.security.ldap.QuarkusDirContextFactory; +import io.quarkus.elytron.security.ldap.config.LdapSecurityRealmConfig; +import io.quarkus.runtime.RuntimeValue; + +class ElytronSecurityLdapProcessor { + + LdapSecurityRealmConfig ldap; + + @BuildStep + CapabilityBuildItem capability() { + return new CapabilityBuildItem(Capabilities.SECURITY_ELYTRON_LDAP); + } + + @BuildStep() + FeatureBuildItem feature() { + return new FeatureBuildItem(FeatureBuildItem.SECURITY_LDAP); + } + + /** + * Check to see if a LdapRealmConfig was specified and enabled and create a + * {@linkplain org.wildfly.security.auth.realm.ldap.LdapSecurityRealm} + * + * @param recorder - runtime security recorder + * @param securityRealm - the producer factory for the SecurityRealmBuildItem + * @throws Exception - on any failure + */ + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void configureLdapRealmAuthConfig(LdapRecorder recorder, + BuildProducer securityRealm, + BeanContainerBuildItem beanContainerBuildItem //we need this to make sure ArC is initialized + ) throws Exception { + if (ldap.enabled) { + RuntimeValue realm = recorder.createRealm(ldap); + securityRealm.produce(new SecurityRealmBuildItem(realm, ldap.realmName, null)); + } + } + + @BuildStep + ElytronPasswordMarkerBuildItem marker() { + if (ldap.enabled) { + return new ElytronPasswordMarkerBuildItem(); + } + return null; + } + + @BuildStep + JniBuildItem enableJni() { + return new JniBuildItem(); + } + + @BuildStep + ReflectiveClassBuildItem enableReflection() { + return new ReflectiveClassBuildItem(true, true, QuarkusDirContextFactory.INITIAL_CONTEXT_FACTORY); + } +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoder.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoder.java new file mode 100644 index 0000000000000..9697832847407 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoder.java @@ -0,0 +1,28 @@ +package io.quarkus.elytron.security.ldap; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.StreamSupport; + +import javax.enterprise.context.ApplicationScoped; + +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.authz.RoleDecoder; +import org.wildfly.security.authz.Roles; + +@ApplicationScoped +public class CustomRoleDecoder implements RoleDecoder { + + @Override + public Roles decodeRoles(AuthorizationIdentity authorizationIdentity) { + Attributes.Entry groupsEntry = authorizationIdentity.getAttributes().get("Roles"); + Set roles = new HashSet<>(); + StreamSupport.stream(groupsEntry.spliterator(), false).forEach(groups -> { + for (String role : groups.split(",")) { + roles.add(role.trim()); + } + }); + return Roles.fromSet(roles); + } +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoderTest.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoderTest.java new file mode 100644 index 0000000000000..e4be68b51a431 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoderTest.java @@ -0,0 +1,24 @@ +package io.quarkus.elytron.security.ldap; + +import java.util.Arrays; +import java.util.stream.Stream; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class CustomRoleDecoderTest extends LdapSecurityRealmTest { + + static Class[] testClassesWithCustomRoleDecoder = Stream.concat( + Arrays.stream(testClasses), + Arrays.stream(new Class[] { CustomRoleDecoder.class })).toArray(Class[]::new); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(testClassesWithCustomRoleDecoder) + .addAsResource("custom-role-decoder/application.properties", "application.properties")); + +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/LdapSecurityRealmTest.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/LdapSecurityRealmTest.java new file mode 100644 index 0000000000000..ed589a1af7aed --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/LdapSecurityRealmTest.java @@ -0,0 +1,152 @@ +package io.quarkus.elytron.security.ldap; + +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; + +import io.quarkus.elytron.security.ldap.rest.ParametrizedPathsResource; +import io.quarkus.elytron.security.ldap.rest.RolesEndpointClassLevel; +import io.quarkus.elytron.security.ldap.rest.SingleRoleSecuredServlet; +import io.quarkus.elytron.security.ldap.rest.SubjectExposingResource; +import io.quarkus.elytron.security.ldap.rest.TestApplication; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.ldap.LdapServerTestResource; +import io.restassured.RestAssured; + +/** + * Tests of BASIC authentication mechanism with the minimal config required + */ + +@QuarkusTestResource(LdapServerTestResource.class) +public abstract class LdapSecurityRealmTest { + + protected static Class[] testClasses = { + SingleRoleSecuredServlet.class, TestApplication.class, RolesEndpointClassLevel.class, + ParametrizedPathsResource.class, SubjectExposingResource.class + }; + + // Basic @ServletSecurity tests + @Test() + public void testSecureAccessFailure() { + RestAssured.when().get("/servlet-secured").then() + .statusCode(401); + } + + @Test() + public void testSecureRoleFailure() { + RestAssured.given().auth().preemptive().basic("noRoleUser", "noRoleUserPassword") + .when().get("/servlet-secured").then() + .statusCode(403); + } + + @Test() + public void testSecureAccessSuccess() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/servlet-secured").then() + .statusCode(200); + } + + /** + * Test access a secured jaxrs resource without any authentication. should see 401 error code. + */ + @Test + public void testJaxrsGetFailure() { + RestAssured.when().get("/jaxrs-secured/roles-class").then() + .statusCode(401); + } + + /** + * Test access a secured jaxrs resource with authentication, but no authorization. should see 403 error code. + */ + @Test + public void testJaxrsGetRoleFailure() { + RestAssured.given().auth().preemptive().basic("noRoleUser", "noRoleUserPassword") + .when().get("/jaxrs-secured/roles-class").then() + .statusCode(403); + } + + /** + * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code. + */ + @Test + public void testJaxrsGetRoleSuccess() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/jaxrs-secured/roles-class").then() + .statusCode(200); + } + + /** + * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code. + */ + @Test + public void testJaxrsPathAdminRoleSuccess() { + RestAssured.given().auth().preemptive().basic("adminUser", "adminUserPassword") + .when().get("/jaxrs-secured/parameterized-paths/my/banking/admin").then() + .statusCode(200); + } + + @Test + public void testJaxrsPathAdminRoleFailure() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/jaxrs-secured/parameterized-paths/my/banking/admin").then() + .statusCode(403); + } + + /** + * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code. + */ + @Test + public void testJaxrsPathUserRoleSuccess() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/jaxrs-secured/parameterized-paths/my/banking/view").then() + .statusCode(200); + } + + /** + * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code. + */ + @Test + public void testJaxrsUserRoleSuccess() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/jaxrs-secured/subject/secured").then() + .statusCode(200) + .body(equalTo("standardUser")); + } + + @Test + public void testJaxrsInjectedPrincipalSuccess() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/jaxrs-secured/subject/principal-secured").then() + .statusCode(200) + .body(equalTo("standardUser")); + } + + /** + * Test access a @PermitAll secured jaxrs resource without any authentication. should see a 200 success code. + */ + @Test + public void testJaxrsGetPermitAll() { + RestAssured.when().get("/jaxrs-secured/subject/unsecured").then() + .statusCode(200) + .body(equalTo("anonymous")); + } + + /** + * Test access a @DenyAll secured jaxrs resource without authentication. should see a 401 success code. + */ + @Test + public void testJaxrsGetDenyAllWithoutAuth() { + RestAssured.when().get("/jaxrs-secured/subject/denied").then() + .statusCode(401); + } + + /** + * Test access a @DenyAll secured jaxrs resource with authentication. should see a 403 success code. + */ + @Test + public void testJaxrsGetDenyAllWithAuth() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/jaxrs-secured/subject/denied").then() + .statusCode(403); + } +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/MinimalConfigurationTest.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/MinimalConfigurationTest.java new file mode 100644 index 0000000000000..6ae33d5b81698 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/MinimalConfigurationTest.java @@ -0,0 +1,17 @@ +package io.quarkus.elytron.security.ldap; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MinimalConfigurationTest extends LdapSecurityRealmTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(testClasses) + .addAsResource("minimal-config/application.properties", "application.properties")); + +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/ParametrizedPathsResource.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/ParametrizedPathsResource.java new file mode 100644 index 0000000000000..33d8be9dfdf78 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/ParametrizedPathsResource.java @@ -0,0 +1,23 @@ +package io.quarkus.elytron.security.ldap.rest; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +@Path("/parameterized-paths") +public class ParametrizedPathsResource { + @GET + @Path("/my/{path}/admin") + @RolesAllowed("adminRole") + public String admin(@PathParam("path") String path) { + return "Admin accessed " + path; + } + + @GET + @Path("/my/{path}/view") + @RolesAllowed("standardRole") + public String view(@PathParam("path") String path) { + return "View accessed " + path; + } +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/RolesEndpointClassLevel.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/RolesEndpointClassLevel.java new file mode 100644 index 0000000000000..fc8e8f8b8ebed --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/RolesEndpointClassLevel.java @@ -0,0 +1,20 @@ +package io.quarkus.elytron.security.ldap.rest; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +/** + * Test JAXRS endpoint with RolesAllowed specified at the class level + */ +@Path("/roles-class") +@RolesAllowed("standardRole") +public class RolesEndpointClassLevel { + @GET + public String echo(@Context SecurityContext sec) { + return "Hello " + sec.getUserPrincipal().getName(); + } + +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SingleRoleSecuredServlet.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SingleRoleSecuredServlet.java new file mode 100644 index 0000000000000..a9c6df3421306 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SingleRoleSecuredServlet.java @@ -0,0 +1,24 @@ +package io.quarkus.elytron.security.ldap.rest; + +import java.io.IOException; + +import javax.servlet.annotation.HttpConstraint; +import javax.servlet.annotation.ServletSecurity; +import javax.servlet.annotation.WebInitParam; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Basic secured servlet test target + */ +@ServletSecurity(@HttpConstraint(rolesAllowed = { "standardRole" })) +@WebServlet(name = "SingleRoleSecuredServlet", urlPatterns = "/servlet-secured", initParams = { + @WebInitParam(name = "message", value = "A secured message") }) +public class SingleRoleSecuredServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.getWriter().write(getInitParameter("message")); + } +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SubjectExposingResource.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SubjectExposingResource.java new file mode 100644 index 0000000000000..124a0a7f704e7 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SubjectExposingResource.java @@ -0,0 +1,57 @@ +package io.quarkus.elytron.security.ldap.rest; + +import java.security.Principal; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("subject") +public class SubjectExposingResource { + + @Inject + Principal principal; + + @GET + @RolesAllowed("standardRole") + @Path("secured") + public String getSubjectSecured(@Context SecurityContext sec) { + Principal user = sec.getUserPrincipal(); + String name = user != null ? user.getName() : "anonymous"; + return name; + } + + @GET + @RolesAllowed("standardRole") + @Path("principal-secured") + public String getPrincipalSecured(@Context SecurityContext sec) { + if (principal == null) { + throw new IllegalStateException("No injected principal"); + } + String name = principal.getName(); + return name; + } + + @GET + @Path("unsecured") + @PermitAll + public String getSubjectUnsecured(@Context SecurityContext sec) { + Principal user = sec.getUserPrincipal(); + String name = user != null ? user.getName() : "anonymous"; + return name; + } + + @DenyAll + @GET + @Path("denied") + public String getSubjectDenied(@Context SecurityContext sec) { + Principal user = sec.getUserPrincipal(); + String name = user != null ? user.getName() : "anonymous"; + return name; + } +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/TestApplication.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/TestApplication.java new file mode 100644 index 0000000000000..752efd59d42e6 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/TestApplication.java @@ -0,0 +1,11 @@ +package io.quarkus.elytron.security.ldap.rest; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationScoped +@ApplicationPath("/jaxrs-secured") +public class TestApplication extends Application { + // intentionally left empty +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/resources/custom-role-decoder/application.properties b/extensions/elytron-security-ldap/deployment/src/test/resources/custom-role-decoder/application.properties new file mode 100644 index 0000000000000..4e5cf424913d3 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/resources/custom-role-decoder/application.properties @@ -0,0 +1,13 @@ +quarkus.security.ldap.enabled=true + +quarkus.security.ldap.dir-context.principal=uid=admin,ou=system +quarkus.security.ldap.dir-context.url=ldap://127.0.0.1:10389 +quarkus.security.ldap.dir-context.password=secret + +quarkus.security.ldap.identity-mapping.rdn-identifier=uid +quarkus.security.ldap.identity-mapping.search-base-dn=ou=Users,dc=quarkus,dc=io + +quarkus.security.ldap.identity-mapping.attribute-mappings."0".from=cn +quarkus.security.ldap.identity-mapping.attribute-mappings."0".to=Roles +quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter=(member=uid={0},ou=Users,dc=quarkus,dc=io) +quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=Roles,dc=quarkus,dc=io \ No newline at end of file diff --git a/extensions/elytron-security-ldap/deployment/src/test/resources/minimal-config/application.properties b/extensions/elytron-security-ldap/deployment/src/test/resources/minimal-config/application.properties new file mode 100644 index 0000000000000..e9282033bed8b --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/resources/minimal-config/application.properties @@ -0,0 +1,12 @@ +quarkus.security.ldap.enabled=true + +quarkus.security.ldap.dir-context.principal=uid=admin,ou=system +quarkus.security.ldap.dir-context.url=ldap://127.0.0.1:10389 +quarkus.security.ldap.dir-context.password=secret + +quarkus.security.ldap.identity-mapping.rdn-identifier=uid +quarkus.security.ldap.identity-mapping.search-base-dn=ou=Users,dc=quarkus,dc=io + +quarkus.security.ldap.identity-mapping.attribute-mappings."0".from=cn +quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter=(member=uid={0},ou=Users,dc=quarkus,dc=io) +quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=Roles,dc=quarkus,dc=io \ No newline at end of file diff --git a/extensions/elytron-security-ldap/pom.xml b/extensions/elytron-security-ldap/pom.xml new file mode 100644 index 0000000000000..c7f782508322a --- /dev/null +++ b/extensions/elytron-security-ldap/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + io.quarkus + quarkus-extensions-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-elytron-security-ldap-parent + Quarkus - Elytron Security LDAP + + pom + + deployment + runtime + + diff --git a/extensions/elytron-security-ldap/runtime/pom.xml b/extensions/elytron-security-ldap/runtime/pom.xml new file mode 100644 index 0000000000000..c97b7bb7c9fbd --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + io.quarkus + quarkus-elytron-security-ldap-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-elytron-security-ldap + Quarkus - Elytron Security LDAP - Runtime + Secure your applications with username/password via LDAP + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-elytron-security + + + io.quarkus + quarkus-arc + + + org.wildfly.security + wildfly-elytron-realm-ldap + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java new file mode 100644 index 0000000000000..c90a9da278e2c --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java @@ -0,0 +1,497 @@ +package io.quarkus.elytron.security.ldap; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Hashtable; + +import javax.naming.Binding; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.NameParser; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.ReferralException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.Control; +import javax.naming.ldap.ExtendedRequest; +import javax.naming.ldap.ExtendedResponse; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import javax.net.SocketFactory; + +import org.wildfly.common.Assert; +import org.wildfly.security.auth.realm.ldap.ThreadLocalSSLSocketFactory; +import org.wildfly.security.manager.action.SetContextClassLoaderAction; + +class DelegatingLdapContext implements LdapContext { + + private final DirContext delegating; + private final CloseHandler closeHandler; + private final SocketFactory socketFactory; + + interface CloseHandler { + void handle(DirContext context) throws NamingException; + } + + DelegatingLdapContext(DirContext delegating, CloseHandler closeHandler, SocketFactory socketFactory) + throws NamingException { + this.delegating = delegating; + this.closeHandler = closeHandler; + this.socketFactory = socketFactory; + } + + // for needs of newInstance() + private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) throws NamingException { + this.delegating = delegating; + this.closeHandler = null; // close handler should not be applied to copy + this.socketFactory = socketFactory; + } + + public LdapContext newInitialLdapContext(Hashtable environment, Control[] connCtls) throws NamingException { + ClassLoader previous = setSocketFactory(); + try { + return new InitialLdapContext(environment, null); + } finally { + unsetSocketFactory(previous); + } + } + + @Override + public void close() throws NamingException { + if (closeHandler == null) { + delegating.close(); + } else { + closeHandler.handle(delegating); + } + } + + // for needs of search() + private NamingEnumeration wrap(NamingEnumeration delegating) { + return new NamingEnumeration() { + + @Override + public boolean hasMoreElements() { + ClassLoader previous = setSocketFactory(); + try { + return delegating.hasMoreElements(); + } finally { + unsetSocketFactory(previous); + } + } + + @Override + public SearchResult nextElement() { + ClassLoader previous = setSocketFactory(); + try { + return delegating.nextElement(); + } finally { + unsetSocketFactory(previous); + } + } + + @Override + public SearchResult next() throws NamingException { + ClassLoader previous = setSocketFactory(); + try { + return delegating.next(); + } finally { + unsetSocketFactory(previous); + } + } + + @Override + public boolean hasMore() throws NamingException { + ClassLoader previous = setSocketFactory(); + try { + return delegating.hasMore(); + } finally { + unsetSocketFactory(previous); + } + } + + @Override + public void close() throws NamingException { + delegating.close(); + } + }; + } + + public DelegatingLdapContext wrapReferralContextObtaining(ReferralException e) throws NamingException { + ClassLoader previous = setSocketFactory(); + try { + return new DelegatingLdapContext((DirContext) e.getReferralContext(), socketFactory); + } finally { + unsetSocketFactory(previous); + } + } + + @Override + public String toString() { + return super.toString() + "->" + delegating.toString(); + } + + // LdapContext specific + + @Override + public ExtendedResponse extendedOperation(ExtendedRequest request) throws NamingException { + if (!(delegating instanceof LdapContext)) + throw Assert.unsupported(); + return ((LdapContext) delegating).extendedOperation(request); + } + + @Override + public LdapContext newInstance(Control[] requestControls) throws NamingException { + if (!(delegating instanceof LdapContext)) + throw Assert.unsupported(); + LdapContext newContext = ((LdapContext) delegating).newInstance(requestControls); + return new DelegatingLdapContext(newContext, socketFactory); + } + + @Override + public void reconnect(Control[] controls) throws NamingException { + if (!(delegating instanceof LdapContext)) + throw Assert.unsupported(); + ClassLoader previous = setSocketFactory(); + try { + ((LdapContext) delegating).reconnect(controls); + } finally { + unsetSocketFactory(previous); + } + } + + @Override + public Control[] getConnectControls() throws NamingException { + if (!(delegating instanceof LdapContext)) + throw Assert.unsupported(); + return ((LdapContext) delegating).getConnectControls(); + } + + @Override + public void setRequestControls(Control[] requestControls) throws NamingException { + if (!(delegating instanceof LdapContext)) + throw Assert.unsupported(); + ((LdapContext) delegating).setRequestControls(requestControls); + } + + @Override + public Control[] getRequestControls() throws NamingException { + if (!(delegating instanceof LdapContext)) + throw Assert.unsupported(); + return ((LdapContext) delegating).getRequestControls(); + } + + @Override + public Control[] getResponseControls() throws NamingException { + if (!(delegating instanceof LdapContext)) + throw Assert.unsupported(); + return ((LdapContext) delegating).getResponseControls(); + } + + // DirContext methods delegates only + + @Override + public void bind(String name, Object obj, Attributes attrs) throws NamingException { + delegating.bind(name, obj, attrs); + } + + @Override + public Attributes getAttributes(Name name) throws NamingException { + return delegating.getAttributes(name); + } + + @Override + public Attributes getAttributes(String name) throws NamingException { + return delegating.getAttributes(name); + } + + @Override + public Attributes getAttributes(Name name, String[] attrIds) throws NamingException { + return delegating.getAttributes(name, attrIds); + } + + @Override + public Attributes getAttributes(String name, String[] attrIds) throws NamingException { + return delegating.getAttributes(name, attrIds); + } + + @Override + public void modifyAttributes(Name name, int mod_op, Attributes attrs) throws NamingException { + delegating.modifyAttributes(name, mod_op, attrs); + } + + @Override + public void modifyAttributes(String name, int mod_op, Attributes attrs) throws NamingException { + delegating.modifyAttributes(name, mod_op, attrs); + } + + @Override + public void modifyAttributes(Name name, ModificationItem[] mods) throws NamingException { + delegating.modifyAttributes(name, mods); + } + + @Override + public void modifyAttributes(String name, ModificationItem[] mods) throws NamingException { + delegating.modifyAttributes(name, mods); + } + + @Override + public void bind(Name name, Object obj, Attributes attrs) throws NamingException { + delegating.bind(name, obj, attrs); + } + + @Override + public void rebind(Name name, Object obj, Attributes attrs) throws NamingException { + delegating.rebind(name, obj, attrs); + } + + @Override + public void rebind(String name, Object obj, Attributes attrs) throws NamingException { + delegating.rebind(name, obj, attrs); + } + + @Override + public DirContext createSubcontext(Name name, Attributes attrs) throws NamingException { + return delegating.createSubcontext(name, attrs); + } + + @Override + public DirContext createSubcontext(String name, Attributes attrs) throws NamingException { + return delegating.createSubcontext(name, attrs); + } + + @Override + public DirContext getSchema(Name name) throws NamingException { + return delegating.getSchema(name); + } + + @Override + public DirContext getSchema(String name) throws NamingException { + return delegating.getSchema(name); + } + + @Override + public DirContext getSchemaClassDefinition(Name name) throws NamingException { + return delegating.getSchemaClassDefinition(name); + } + + @Override + public DirContext getSchemaClassDefinition(String name) throws NamingException { + return delegating.getSchemaClassDefinition(name); + } + + @Override + public NamingEnumeration search(Name name, Attributes matchingAttributes, String[] attributesToReturn) + throws NamingException { + return wrap(delegating.search(name, matchingAttributes, attributesToReturn)); + } + + @Override + public NamingEnumeration search(String name, Attributes matchingAttributes, String[] attributesToReturn) + throws NamingException { + return wrap(delegating.search(name, matchingAttributes, attributesToReturn)); + } + + @Override + public NamingEnumeration search(Name name, Attributes matchingAttributes) throws NamingException { + return wrap(delegating.search(name, matchingAttributes)); + } + + @Override + public NamingEnumeration search(String name, Attributes matchingAttributes) throws NamingException { + return wrap(delegating.search(name, matchingAttributes)); + } + + @Override + public NamingEnumeration search(Name name, String filter, SearchControls cons) throws NamingException { + return wrap(delegating.search(name, filter, cons)); + } + + @Override + public NamingEnumeration search(String name, String filter, SearchControls cons) throws NamingException { + return wrap(delegating.search(name, filter, cons)); + } + + @Override + public NamingEnumeration search(Name name, String filterExpr, Object[] filterArgs, SearchControls cons) + throws NamingException { + return wrap(delegating.search(name, filterExpr, filterArgs, cons)); + } + + @Override + public NamingEnumeration search(String name, String filterExpr, Object[] filterArgs, SearchControls cons) + throws NamingException { + return wrap(delegating.search(name, filterExpr, filterArgs, cons)); + } + + @Override + public Object lookup(Name name) throws NamingException { + return delegating.lookup(name); + } + + @Override + public Object lookup(String name) throws NamingException { + return delegating.lookup(name); + } + + @Override + public void bind(Name name, Object obj) throws NamingException { + delegating.bind(name, obj); + } + + @Override + public void bind(String name, Object obj) throws NamingException { + delegating.bind(name, obj); + } + + @Override + public void rebind(Name name, Object obj) throws NamingException { + delegating.rebind(name, obj); + } + + @Override + public void rebind(String name, Object obj) throws NamingException { + delegating.rebind(name, obj); + } + + @Override + public void unbind(Name name) throws NamingException { + delegating.unbind(name); + } + + @Override + public void unbind(String name) throws NamingException { + delegating.unbind(name); + } + + @Override + public void rename(Name oldName, Name newName) throws NamingException { + delegating.rename(oldName, newName); + } + + @Override + public void rename(String oldName, String newName) throws NamingException { + delegating.rename(oldName, newName); + } + + @Override + public NamingEnumeration list(Name name) throws NamingException { + return delegating.list(name); + } + + @Override + public NamingEnumeration list(String name) throws NamingException { + return delegating.list(name); + } + + @Override + public NamingEnumeration listBindings(Name name) throws NamingException { + return delegating.listBindings(name); + } + + @Override + public NamingEnumeration listBindings(String name) throws NamingException { + return delegating.listBindings(name); + } + + @Override + public void destroySubcontext(Name name) throws NamingException { + delegating.destroySubcontext(name); + } + + @Override + public void destroySubcontext(String name) throws NamingException { + delegating.destroySubcontext(name); + } + + @Override + public Context createSubcontext(Name name) throws NamingException { + return delegating.createSubcontext(name); + } + + @Override + public Context createSubcontext(String name) throws NamingException { + return delegating.createSubcontext(name); + } + + @Override + public Object lookupLink(Name name) throws NamingException { + return delegating.lookupLink(name); + } + + @Override + public Object lookupLink(String name) throws NamingException { + return delegating.lookupLink(name); + } + + @Override + public NameParser getNameParser(Name name) throws NamingException { + return delegating.getNameParser(name); + } + + @Override + public NameParser getNameParser(String name) throws NamingException { + return delegating.getNameParser(name); + } + + @Override + public Name composeName(Name name, Name prefix) throws NamingException { + return delegating.composeName(name, prefix); + } + + @Override + public String composeName(String name, String prefix) throws NamingException { + return delegating.composeName(name, prefix); + } + + @Override + public Object addToEnvironment(String propName, Object propVal) throws NamingException { + return delegating.addToEnvironment(propName, propVal); + } + + @Override + public Object removeFromEnvironment(String propName) throws NamingException { + return delegating.removeFromEnvironment(propName); + } + + @Override + public Hashtable getEnvironment() throws NamingException { + return delegating.getEnvironment(); + } + + @Override + public String getNameInNamespace() throws NamingException { + return delegating.getNameInNamespace(); + } + + private ClassLoader setSocketFactory() { + if (socketFactory != null) { + ThreadLocalSSLSocketFactory.set(socketFactory); + return setClassLoaderTo(getSocketFactoryClassLoader()); + } + return null; + } + + private void unsetSocketFactory(ClassLoader previous) { + if (socketFactory != null) { + ThreadLocalSSLSocketFactory.unset(); + setClassLoaderTo(previous); + } + } + + private ClassLoader getSocketFactoryClassLoader() { + return ThreadLocalSSLSocketFactory.class.getClassLoader(); + } + + private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { + return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + } + + private static T doPrivileged(final PrivilegedAction action) { + return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); + } +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/LdapRecorder.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/LdapRecorder.java new file mode 100644 index 0000000000000..d0431f3dc1b0b --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/LdapRecorder.java @@ -0,0 +1,81 @@ +package io.quarkus.elytron.security.ldap; + +import java.security.Provider; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +import org.wildfly.common.function.ExceptionSupplier; +import org.wildfly.security.WildFlyElytronProvider; +import org.wildfly.security.auth.realm.ldap.AttributeMapping; +import org.wildfly.security.auth.realm.ldap.DirContextFactory; +import org.wildfly.security.auth.realm.ldap.LdapSecurityRealmBuilder; +import org.wildfly.security.auth.server.SecurityRealm; + +import io.quarkus.elytron.security.ldap.config.AttributeMappingConfig; +import io.quarkus.elytron.security.ldap.config.DirContextConfig; +import io.quarkus.elytron.security.ldap.config.IdentityMappingConfig; +import io.quarkus.elytron.security.ldap.config.LdapSecurityRealmConfig; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class LdapRecorder { + + private static final Provider[] PROVIDERS = new Provider[] { new WildFlyElytronProvider() }; + + /** + * Create a runtime value for a {@linkplain LdapSecurityRealm} + * + * @param config - the realm config + * @return - runtime value wrapper for the SecurityRealm + */ + public RuntimeValue createRealm(LdapSecurityRealmConfig config) { + Supplier providers = new Supplier() { + @Override + public Provider[] get() { + return PROVIDERS; + } + }; + LdapSecurityRealmBuilder builder = LdapSecurityRealmBuilder.builder() + .setDirContextSupplier(createDirContextSupplier(config.dirContext)) + .setProviders(providers) + .identityMapping() + .map(createAttributeMappings(config.identityMapping)) + .setRdnIdentifier(config.identityMapping.rdnIdentifier) + .setSearchDn(config.identityMapping.searchBaseDn) + .build(); + + if (config.directVerification) { + builder.addDirectEvidenceVerification(false); + } + + return new RuntimeValue<>(builder.build()); + } + + private ExceptionSupplier createDirContextSupplier(DirContextConfig dirContext) { + DirContextFactory dirContextFactory = new QuarkusDirContextFactory( + dirContext.url, + dirContext.principal, + dirContext.password); + return () -> dirContextFactory.obtainDirContext(DirContextFactory.ReferralMode.IGNORE); + } + + private AttributeMapping[] createAttributeMappings(IdentityMappingConfig identityMappingConfig) { + List attributeMappings = new ArrayList<>(); + + for (AttributeMappingConfig attributeMappingConfig : identityMappingConfig.attributeMappings.values()) { + attributeMappings.add(AttributeMapping.fromFilter(attributeMappingConfig.filter) + .from(attributeMappingConfig.from) + .to(attributeMappingConfig.to) + .searchDn(attributeMappingConfig.filterBaseDn) + .build()); + } + + AttributeMapping[] attributeMappingsArray = new AttributeMapping[attributeMappings.size()]; + return attributeMappings.toArray(attributeMappingsArray); + } +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java new file mode 100644 index 0000000000000..6dde3648e0826 --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java @@ -0,0 +1,151 @@ +package io.quarkus.elytron.security.ldap; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Hashtable; + +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.ldap.InitialLdapContext; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; + +import org.wildfly.security.auth.realm.ldap.DirContextFactory; +import org.wildfly.security.manager.action.SetContextClassLoaderAction; + +public class QuarkusDirContextFactory implements DirContextFactory { + // private static final ElytronMessages log = Logger.getMessageLogger(ElytronMessages.class, "org.wildfly.security"); + + private static final String CONNECT_TIMEOUT = "com.sun.jndi.ldap.connect.timeout"; + private static final String READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout"; + private static final String SOCKET_FACTORY = "java.naming.ldap.factory.socket"; + public static final String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; + private static final String SECURITY_AUTHENTICATION = "simple"; + + private static final String DEFAULT_CONNECT_TIMEOUT = "5000"; // ms + private static final String DEFAULT_READ_TIMEOUT = "60000"; // ms + private static final String LDAPS_SCHEME = "ldaps"; + + private final String providerUrl; + private final String securityPrincipal; + private final String securityCredential; + private final ClassLoader targetClassLoader; + + public QuarkusDirContextFactory(String providerUrl, String securityPrincipal, String securityCredential) { + this.providerUrl = providerUrl; + this.securityPrincipal = securityPrincipal; + this.securityCredential = securityCredential; + this.targetClassLoader = getClass().getClassLoader(); + } + + @Override + public DirContext obtainDirContext(ReferralMode mode) throws NamingException { + char[] charPassword = null; + if (securityCredential != null) { // password from String + charPassword = securityCredential.toCharArray(); + } + return createDirContext(securityPrincipal, charPassword, mode); + } + + @Override + public DirContext obtainDirContext(CallbackHandler handler, ReferralMode mode) throws NamingException { + NameCallback nameCallback = new NameCallback("Principal Name"); + PasswordCallback passwordCallback = new PasswordCallback("Password", false); + + try { + handler.handle(new Callback[] { nameCallback, passwordCallback }); + } catch (Exception e) { + throw new RuntimeException("Could not obtain credential", e); + // throw log.couldNotObtainCredentialWithCause(e); + } + + String securityPrincipal = nameCallback.getName(); + + if (securityPrincipal == null) { + throw new RuntimeException("Could not obtain principal"); + // throw log.couldNotObtainPrincipal(); + } + + char[] securityCredential = passwordCallback.getPassword(); + + if (securityCredential == null) { + throw new RuntimeException("Could not obtain credential"); + // throw log.couldNotObtainCredential(); + } + + return createDirContext(securityPrincipal, securityCredential, mode); + } + + private DirContext createDirContext(String securityPrincipal, char[] securityCredential, ReferralMode mode) + throws NamingException { + final ClassLoader oldClassLoader = setClassLoaderTo(targetClassLoader); + try { + Hashtable env = new Hashtable<>(); + + env.put(InitialDirContext.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY); + env.put(InitialDirContext.PROVIDER_URL, providerUrl); + env.put(InitialDirContext.SECURITY_AUTHENTICATION, SECURITY_AUTHENTICATION); + if (securityPrincipal != null) { + env.put(InitialDirContext.SECURITY_PRINCIPAL, securityPrincipal); + } + if (securityCredential != null) { + env.put(InitialDirContext.SECURITY_CREDENTIALS, securityCredential); + } + env.put(InitialDirContext.REFERRAL, mode == null ? ReferralMode.IGNORE.getValue() : mode.getValue()); + env.put(CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT); + env.put(READ_TIMEOUT, DEFAULT_READ_TIMEOUT); + + // if (log.isDebugEnabled()) { + // log.debugf("Creating [" + InitialDirContext.class + "] with environment:"); + // env.forEach((key, value) -> log.debugf(" Property [%s] with value [%s]", key, + // key != InitialDirContext.SECURITY_CREDENTIALS ? Arrays2.objectToString(value) : "******")); + // } + + InitialLdapContext initialContext; + + try { + initialContext = new InitialLdapContext(env, null); + } catch (NamingException ne) { + // log.debugf(ne, "Could not create [%s]. Failed to connect to LDAP server.", InitialLdapContext.class); + throw ne; + } + + // log.debugf("[%s] successfully created. Connection established to LDAP server.", initialContext); + + return new DelegatingLdapContext(initialContext, this::returnContext, null); + } finally { + setClassLoaderTo(oldClassLoader); + } + } + + @Override + public void returnContext(DirContext context) { + + if (context == null) { + return; + } + + if (context instanceof InitialDirContext) { + final ClassLoader oldClassLoader = setClassLoaderTo(targetClassLoader); + try { + context.close(); + // log.debugf("Context [%s] was closed. Connection closed or just returned to the pool.", context); + } catch (NamingException ignored) { + } finally { + setClassLoaderTo(oldClassLoader); + } + } + } + + private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { + return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + } + + private static T doPrivileged(final PrivilegedAction action) { + return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); + } + +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/AttributeMappingConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/AttributeMappingConfig.java new file mode 100644 index 0000000000000..5a933bf81e9de --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/AttributeMappingConfig.java @@ -0,0 +1,35 @@ +package io.quarkus.elytron.security.ldap.config; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration information used to populate a {@linkplain org.wildfly.security.auth.realm.ldap.AttributeMapping} + */ +@ConfigGroup +public class AttributeMappingConfig { + + /** + * The roleAttributeId from which is mapped (e.g. "cn") + */ + @ConfigItem + public String from; + + /** + * The identifier whom the attribute is mapped to (in Quarkus: "groups", in WildFly this is "Roles") + */ + @ConfigItem(defaultValue = "groups") + public String to; + + /** + * The filter (also named "roleFilter") + */ + @ConfigItem + public String filter; + + /** + * The filter base dn (also named "rolesContextDn") + */ + @ConfigItem + public String filterBaseDn; +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/DirContextConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/DirContextConfig.java new file mode 100644 index 0000000000000..95476076b255f --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/DirContextConfig.java @@ -0,0 +1,35 @@ +package io.quarkus.elytron.security.ldap.config; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class DirContextConfig { + + /** + * The url of the ldap server + */ + @ConfigItem + public String url; + + /** + * The principal: user which is used to connect to ldap server (also named "bindDn") + */ + @ConfigItem + public String principal; + + /** + * The password which belongs to the principal (also named "bindCredential") + */ + @ConfigItem + public String password; + + @Override + public String toString() { + return "DirContextConfig{" + + "url='" + url + '\'' + + ", principal='" + principal + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/IdentityMappingConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/IdentityMappingConfig.java new file mode 100644 index 0000000000000..51a6edc2981f8 --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/IdentityMappingConfig.java @@ -0,0 +1,37 @@ +package io.quarkus.elytron.security.ldap.config; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class IdentityMappingConfig { + + /** + * The identifier which correlates to the provided user (e.g. "uid") + */ + @ConfigItem + public String rdnIdentifier; + + /** + * The dn where we look for users + */ + @ConfigItem + public String searchBaseDn; + + /** + * The configs how we get from the attribute to the Role + */ + @ConfigItem + public Map attributeMappings; + + @Override + public String toString() { + return "IdentityMappingConfig{" + + "rdnIdentifier='" + rdnIdentifier + '\'' + + ", searchBaseDn='" + searchBaseDn + '\'' + + ", attributeMappings=" + attributeMappings + + '}'; + } +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmConfig.java new file mode 100644 index 0000000000000..2e8a1015eaa79 --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmConfig.java @@ -0,0 +1,54 @@ +package io.quarkus.elytron.security.ldap.config; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * A configuration object for a jdbc based realm configuration, + * {@linkplain org.wildfly.security.auth.realm.ldap.LdapSecurityRealm} + */ +@ConfigRoot(name = "security.ldap", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class LdapSecurityRealmConfig { + + /** + * The option to enable the ldap elytron module + */ + @ConfigItem + public boolean enabled; + + /** + * The elytron realm name + */ + @ConfigItem(defaultValue = "Quarkus") + public String realmName; + + /** + * Provided credentials are verified against ldap? + */ + @ConfigItem(defaultValue = "true") + public boolean directVerification; + + /** + * The ldap server configuration + */ + @ConfigItem + public DirContextConfig dirContext; + + /** + * The config which we use to map an identity + */ + @ConfigItem + public IdentityMappingConfig identityMapping; + + @Override + public String toString() { + return "LdapSecurityRealmConfig{" + + "enabled=" + enabled + + ", realmName='" + realmName + '\'' + + ", directVerification=" + directVerification + + ", dirContext=" + dirContext + + ", identityMapping=" + identityMapping + + '}'; + } +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/elytron-security-ldap/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..4fc9dcd772342 --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,10 @@ +--- +name: "Elytron Security LDAP Realm" +metadata: + keywords: + - "security" + - "ldap" + guide: "https://quarkus.io/guides/security-ldap" + categories: + - "security" + status: "preview" diff --git a/extensions/elytron-security-oauth2/deployment/src/main/java/io/quarkus/elytron/security/oauth2/deployment/OAuth2DeploymentProcessor.java b/extensions/elytron-security-oauth2/deployment/src/main/java/io/quarkus/elytron/security/oauth2/deployment/OAuth2DeploymentProcessor.java index 86604682a809d..fb3af86bf319c 100644 --- a/extensions/elytron-security-oauth2/deployment/src/main/java/io/quarkus/elytron/security/oauth2/deployment/OAuth2DeploymentProcessor.java +++ b/extensions/elytron-security-oauth2/deployment/src/main/java/io/quarkus/elytron/security/oauth2/deployment/OAuth2DeploymentProcessor.java @@ -1,7 +1,5 @@ package io.quarkus.elytron.security.oauth2.deployment; -import java.util.function.Supplier; - import javax.enterprise.context.ApplicationScoped; import org.wildfly.security.auth.server.SecurityRealm; @@ -60,7 +58,7 @@ ExtensionSslNativeSupportBuildItem activateSslNativeSupport() { * @throws Exception - on any failure */ @BuildStep - @Record(ExecutionTime.STATIC_INIT) + @Record(ExecutionTime.RUNTIME_INIT) AdditionalBeanBuildItem configureOauth2RealmAuthConfig(OAuth2Recorder recorder, BuildProducer securityRealm) throws Exception { if (oauth2.enabled) { @@ -84,7 +82,7 @@ ElytronTokenMarkerBuildItem marker() { RuntimeBeanBuildItem augmentor(OAuth2Recorder recorder) { return RuntimeBeanBuildItem.builder(SecurityIdentityAugmentor.class) .setScope(ApplicationScoped.class) - .setSupplier((Supplier) recorder.augmentor(oauth2)) + .setRuntimeValue(recorder.augmentor(oauth2)) .setRemovable(false) .build(); } diff --git a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/OAuth2Recorder.java b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/OAuth2Recorder.java index e9475d95e775d..023f96b3f2b8d 100644 --- a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/OAuth2Recorder.java +++ b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/OAuth2Recorder.java @@ -13,7 +13,6 @@ import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; -import java.util.function.Supplier; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; @@ -40,6 +39,8 @@ public RuntimeValue createRealm(OAuth2Config config) if (config.caCertFile.isPresent()) { validatorBuilder.useSslContext(createSSLContext(config)); + } else { + validatorBuilder.useSslContext(SSLContext.getDefault()); } OAuth2IntrospectValidator validator = validatorBuilder.build(); @@ -85,13 +86,8 @@ private SSLContext createSSLContext(OAuth2Config config) } } - public Supplier augmentor(OAuth2Config config) { - return new Supplier() { - @Override - public OAuth2Augmentor get() { - return new OAuth2Augmentor(config.roleClaim); - } - }; + public RuntimeValue augmentor(OAuth2Config config) { + return new RuntimeValue<>(new OAuth2Augmentor(config.roleClaim)); } } diff --git a/extensions/elytron-security-properties-file/deployment/src/main/java/io/quarkus/elytron/security/properties/deployment/ElytronPropertiesProcessor.java b/extensions/elytron-security-properties-file/deployment/src/main/java/io/quarkus/elytron/security/properties/deployment/ElytronPropertiesProcessor.java index 2a31a8c8d0986..5bc983551e649 100644 --- a/extensions/elytron-security-properties-file/deployment/src/main/java/io/quarkus/elytron/security/properties/deployment/ElytronPropertiesProcessor.java +++ b/extensions/elytron-security-properties-file/deployment/src/main/java/io/quarkus/elytron/security/properties/deployment/ElytronPropertiesProcessor.java @@ -1,11 +1,8 @@ package io.quarkus.elytron.security.properties.deployment; -import java.util.Set; - import org.jboss.logging.Logger; import org.wildfly.security.auth.server.SecurityRealm; -import io.quarkus.deployment.QuarkusConfig; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -55,14 +52,13 @@ FeatureBuildItem feature() { * to include the build artifact. * * @param recorder - runtime security recorder - * @param resources - NativeImageResourceBuildItem used to register the realm user/roles properties files names. * @param securityRealm - the producer factory for the SecurityRealmBuildItem * @return the AuthConfigBuildItem for the realm authentication mechanism if there was an enabled PropertiesRealmConfig, * null otherwise * @throws Exception - on any failure */ @BuildStep - @Record(ExecutionTime.STATIC_INIT) + @Record(ExecutionTime.RUNTIME_INIT) void configureFileRealmAuthConfig(ElytronPropertiesFileRecorder recorder, BuildProducer resources, BuildProducer securityRealm) throws Exception { @@ -70,8 +66,6 @@ void configureFileRealmAuthConfig(ElytronPropertiesFileRecorder recorder, PropertiesRealmConfig realmConfig = propertiesConfig.file; log.debugf("Configuring from PropertiesRealmConfig, users=%s, roles=%s", realmConfig.users, realmConfig.roles); - // Add the users/roles properties files resource names to build artifact - resources.produce(new NativeImageResourceBuildItem(realmConfig.users, realmConfig.roles)); // Have the runtime recorder create the LegacyPropertiesSecurityRealm and create the build item RuntimeValue realm = recorder.createRealm(realmConfig); securityRealm @@ -80,6 +74,14 @@ void configureFileRealmAuthConfig(ElytronPropertiesFileRecorder recorder, } } + @BuildStep + void nativeResource(BuildProducer resources) throws Exception { + if (propertiesConfig.file.enabled) { + PropertiesRealmConfig realmConfig = propertiesConfig.file; + resources.produce(new NativeImageResourceBuildItem(realmConfig.users, realmConfig.roles)); + } + } + @BuildStep ElytronPasswordMarkerBuildItem marker() { if (propertiesConfig.file.enabled || propertiesConfig.embedded.enabled) { @@ -100,30 +102,12 @@ ElytronPasswordMarkerBuildItem marker() { * @throws Exception - on any failure */ @BuildStep - @Record(ExecutionTime.STATIC_INIT) + @Record(ExecutionTime.RUNTIME_INIT) void configureMPRealmConfig(ElytronPropertiesFileRecorder recorder, BuildProducer securityRealm) throws Exception { if (propertiesConfig.embedded.enabled) { MPRealmConfig realmConfig = propertiesConfig.embedded; log.info("Configuring from MPRealmConfig"); - // These are not being populated correctly by the core config Map logic for some reason, so reparse them here - log.debugf("MPRealmConfig.users: %s", realmConfig.users); - log.debugf("MPRealmConfig.roles: %s", realmConfig.roles); - Set userKeys = QuarkusConfig.getNames(USERS_PREFIX); - - log.debugf("userKeys: %s", userKeys); - for (String key : userKeys) { - String pass = QuarkusConfig.getString(USERS_PREFIX + '.' + key, null, false); - log.debugf("%s.pass = %s", key, pass); - realmConfig.users.put(key, pass); - } - Set roleKeys = QuarkusConfig.getNames(ROLES_PREFIX); - log.debugf("roleKeys: %s", roleKeys); - for (String key : roleKeys) { - String roles = QuarkusConfig.getString(ROLES_PREFIX + '.' + key, null, false); - log.debugf("%s.roles = %s", key, roles); - realmConfig.roles.put(key, roles); - } RuntimeValue realm = recorder.createRealm(realmConfig); securityRealm diff --git a/extensions/elytron-security-properties-file/deployment/src/test/java/io/quarkus/security/test/FormAuthTestCase.java b/extensions/elytron-security-properties-file/deployment/src/test/java/io/quarkus/security/test/FormAuthTestCase.java new file mode 100644 index 0000000000000..219effe9656a1 --- /dev/null +++ b/extensions/elytron-security-properties-file/deployment/src/test/java/io/quarkus/security/test/FormAuthTestCase.java @@ -0,0 +1,54 @@ +package io.quarkus.security.test; + +import static io.restassured.RestAssured.given; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.authentication.FormAuthConfig; + +/** + * Tests of FORM authentication mechanism + *

+ * See @io.quarkus.vertx.http.security.FormAuthTestCase for functional coverage. + */ +public class FormAuthTestCase { + static Class[] testClasses = { + TestSecureServlet.class, TestApplication.class, RolesEndpointClassLevel.class + }; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(testClasses) + .addAsResource("application-form-auth.properties", "application.properties") + .addAsResource("test-users.properties") + .addAsResource("test-roles.properties")); + + @Test() + public void testSecureAccessSuccess() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + given().auth() + .form("stuart", "test", + new FormAuthConfig("j_security_check", "j_username", "j_password") + .withLoggingEnabled()) + .when().get("/secure-test").then().statusCode(200); + + given().auth() + .form("jdoe", "p4ssw0rd", + new FormAuthConfig("j_security_check", "j_username", "j_password") + .withLoggingEnabled()) + .when().get("/secure-test").then().statusCode(403); + + given().auth() + .form("scott", "jb0ss", + new FormAuthConfig("j_security_check", "j_username", "j_password") + .withLoggingEnabled()) + .when().get("/jaxrs-secured/rolesClass").then().statusCode(200); + } + +} diff --git a/extensions/elytron-security-properties-file/deployment/src/test/resources/application-form-auth.properties b/extensions/elytron-security-properties-file/deployment/src/test/resources/application-form-auth.properties new file mode 100644 index 0000000000000..5b1d3aa17e61e --- /dev/null +++ b/extensions/elytron-security-properties-file/deployment/src/test/resources/application-form-auth.properties @@ -0,0 +1,23 @@ +# Logging +#quarkus.log.level=DEBUG +#quarkus.log.console.enable=true +#quarkus.log.console.level=DEBUG +#quarkus.log.file.enable=true +#quarkus.log.file.path=/tmp/trace.log +#quarkus.log.file.level=TRACE +#quarkus.log.file.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n +#quarkus.log.category."io.quarkus.arc".level=TRACE +#quarkus.log.category."io.undertow.request.security".level=TRACE + +# Identities +quarkus.security.users.file.enabled=true +quarkus.security.users.file.users=test-users.properties +quarkus.security.users.file.roles=test-roles.properties +quarkus.security.users.file.plain-text=true + +# Auth method +quarkus.http.auth.form.enabled=true +quarkus.http.auth.basic=false +quarkus.http.auth.form.login-page=/ +quarkus.http.auth.form.error-page=/ +quarkus.http.auth.form.landing-page=/ diff --git a/extensions/elytron-security-properties-file/deployment/src/test/resources/application.properties b/extensions/elytron-security-properties-file/deployment/src/test/resources/application.properties index cec6f162c6e3d..7548cdc43904e 100644 --- a/extensions/elytron-security-properties-file/deployment/src/test/resources/application.properties +++ b/extensions/elytron-security-properties-file/deployment/src/test/resources/application.properties @@ -3,14 +3,14 @@ quarkus.security.users.file.users=test-users.properties quarkus.security.users.file.roles=test-roles.properties quarkus.security.users.file.plain-text=true -quarkus.log.level=DEBUG -quarkus.log.console.enable=true -quarkus.log.console.level=DEBUG -quarkus.log.file.enable=true +#quarkus.log.level=DEBUG +#quarkus.log.console.enable=true +#quarkus.log.console.level=DEBUG +#quarkus.log.file.enable=true # Send output to a trace.log file under the /tmp directory -quarkus.log.file.path=/tmp/trace.log -quarkus.log.file.level=TRACE -quarkus.log.file.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n +#quarkus.log.file.path=/tmp/trace.log +#quarkus.log.file.level=TRACE +#quarkus.log.file.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n # Set 2 categories (io.quarkus.smallrye.jwt, io.undertow.request.security) to TRACE level -quarkus.log.category."io.quarkus.arc".level=TRACE -quarkus.log.category."io.undertow.request.security".level=TRACE +#quarkus.log.category."io.quarkus.arc".level=TRACE +#quarkus.log.category."io.undertow.request.security".level=TRACE diff --git a/extensions/elytron-security-properties-file/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPropertiesFileRecorder.java b/extensions/elytron-security-properties-file/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPropertiesFileRecorder.java index fbad99bf7e10f..75805ad91a405 100644 --- a/extensions/elytron-security-properties-file/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPropertiesFileRecorder.java +++ b/extensions/elytron-security-properties-file/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPropertiesFileRecorder.java @@ -44,6 +44,8 @@ public class ElytronPropertiesFileRecorder { static final Logger log = Logger.getLogger(ElytronPropertiesFileRecorder.class); + private static final Provider[] PROVIDERS = new Provider[] { new WildFlyElytronProvider() }; + /** * Load the user.properties and roles.properties files into the {@linkplain SecurityRealm} * @@ -172,7 +174,7 @@ public RuntimeValue createRealm(PropertiesRealmConfig config) thr .setProviders(new Supplier() { @Override public Provider[] get() { - return new Provider[] { new WildFlyElytronProvider() }; + return PROVIDERS; } }) .setPlainText(config.plainText) @@ -193,7 +195,7 @@ public RuntimeValue createRealm(MPRealmConfig config) { Supplier providers = new Supplier() { @Override public Provider[] get() { - return new Provider[] { new WildFlyElytronProvider() }; + return PROVIDERS; } }; SecurityRealm realm = new SimpleMapBackedSecurityRealm(NameRewriter.IDENTITY_REWRITER, providers); diff --git a/extensions/elytron-security/deployment/pom.xml b/extensions/elytron-security/deployment/pom.xml index d493cac344445..efb2db3824787 100644 --- a/extensions/elytron-security/deployment/pom.xml +++ b/extensions/elytron-security/deployment/pom.xml @@ -25,6 +25,10 @@ quarkus-resteasy-deployment --> + + io.quarkus + quarkus-elytron-security-common-deployment + io.quarkus quarkus-vertx-http-deployment diff --git a/extensions/elytron-security/deployment/src/main/java/io/quarkus/elytron/security/deployment/ElytronDeploymentProcessor.java b/extensions/elytron-security/deployment/src/main/java/io/quarkus/elytron/security/deployment/ElytronDeploymentProcessor.java index ad4e4475d89ed..be3e6efc3adff 100644 --- a/extensions/elytron-security/deployment/src/main/java/io/quarkus/elytron/security/deployment/ElytronDeploymentProcessor.java +++ b/extensions/elytron-security/deployment/src/main/java/io/quarkus/elytron/security/deployment/ElytronDeploymentProcessor.java @@ -75,7 +75,7 @@ void addBeans(BuildProducer beans, List realms) throws Exception { if (realms.size() > 0) { @@ -99,7 +99,13 @@ SecurityDomainBuildItem build(ElytronRecorder recorder, Listio.quarkus quarkus-core + + io.quarkus + quarkus-elytron-security-common + io.quarkus quarkus-vertx-http diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPasswordIdentityProvider.java b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPasswordIdentityProvider.java index 2384973ac4bad..eb673653c1cc4 100644 --- a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPasswordIdentityProvider.java +++ b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronPasswordIdentityProvider.java @@ -62,7 +62,7 @@ public SecurityIdentity get() { throw new RuntimeException(e); } catch (SecurityException e) { log.debug("Authentication failed", e); - throw new AuthenticationFailedException(); + throw new AuthenticationFailedException(e); } } }); diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronRecorder.java b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronRecorder.java index 5400570480958..7c5f38b523215 100644 --- a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronRecorder.java +++ b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronRecorder.java @@ -93,7 +93,15 @@ public void addRealm(RuntimeValue builder, String realmN * @return the security domain runtime value */ public RuntimeValue buildDomain(RuntimeValue builder) { - Security.addProvider(new WildFlyElytronPasswordProvider()); return new RuntimeValue<>(builder.getValue().build()); } + + /** + * As of Graal 19.3.0 this has to be registered at runtime, due to a bug. + * + * 19.3.1 should fix this, see https://github.com/oracle/graal/issues/1883 + */ + public void registerPasswordProvider() { + Security.addProvider(new WildFlyElytronPasswordProvider()); + } } diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTokenIdentityProvider.java b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTokenIdentityProvider.java index 29f9fe6a91ba9..fff7378522aed 100644 --- a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTokenIdentityProvider.java +++ b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTokenIdentityProvider.java @@ -61,7 +61,7 @@ public SecurityIdentity get() { throw new RuntimeException(e); } catch (SecurityException e) { log.debug("Authentication failed", e); - throw new AuthenticationFailedException(); + throw new AuthenticationFailedException(e); } } }); diff --git a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTrustedIdentityProvider.java b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTrustedIdentityProvider.java index fdc1b45333997..9137eee499be3 100644 --- a/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTrustedIdentityProvider.java +++ b/extensions/elytron-security/runtime/src/main/java/io/quarkus/elytron/security/runtime/ElytronTrustedIdentityProvider.java @@ -70,7 +70,7 @@ public SecurityIdentity get() { throw new RuntimeException(e); } catch (SecurityException e) { log.debug("Authentication failed", e); - throw new AuthenticationFailedException(); + throw new AuthenticationFailedException(e); } } }); diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayDatasourceBeanGenerator.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayDatasourceBeanGenerator.java new file mode 100644 index 0000000000000..241721adae31f --- /dev/null +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayDatasourceBeanGenerator.java @@ -0,0 +1,136 @@ +package io.quarkus.flyway; + +import java.util.Collection; +import java.util.HashSet; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Named; +import javax.sql.DataSource; + +import org.flywaydb.core.Flyway; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.util.HashUtil; +import io.quarkus.flyway.runtime.FlywayProducer; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.FieldCreator; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; + +/** + * Generates the CDI producer bean for {@link Flyway} at build time.
+ * Supports multiple {@link Named} {@link DataSource}s. + *

+ * It produces {@link Flyway} instances for every {@link Named} {@link DataSource}. + *

+ * All {@link Flyway} instances get named the same way as the {@link DataSource}s, + * prepended by the prefix {@value #FLYWAY_BEAN_NAME_PREFIX}. + */ +class FlywayDatasourceBeanGenerator { + + public static final String FLYWAY_BEAN_NAME_PREFIX = "flyway_"; + + private static final String FLYWAY_PRODUCER_BEAN_NAME = "FlywayDataSourceProducer"; + private static final String FLYWAY_PRODUCER_PACKAGE_NAME = FlywayProducer.class.getPackage().getName(); + private static final String FLYWAY_PRODUCER_TYPE_NAME = FLYWAY_PRODUCER_PACKAGE_NAME + "." + FLYWAY_PRODUCER_BEAN_NAME; + + private static final int ACCESS_PACKAGE_PROTECTED = 0; + + private final Collection dataSourceNames = new HashSet<>(); + private final BuildProducer generatedBean; + + public FlywayDatasourceBeanGenerator(Collection dataSourceNames, + BuildProducer generatedBean) { + this.dataSourceNames.addAll(dataSourceNames); + this.generatedBean = generatedBean; + } + + /** + * Create a producer bean managing flyway. + *

+ * Build time and runtime configuration are both injected into this bean. + * + * @return String name of the generated producer bean class. + */ + public void createFlywayProducerBean() { + ClassCreator classCreator = ClassCreator.builder() + .classOutput(this::writeGeneratedBeanBuildItem) + .className(FLYWAY_PRODUCER_TYPE_NAME) + .build(); + classCreator.addAnnotation(ApplicationScoped.class); + + FieldCreator defaultProducerField = classCreator.getFieldCreator("defaultProducer", FlywayProducer.class); + defaultProducerField.setModifiers(ACCESS_PACKAGE_PROTECTED); + defaultProducerField.addAnnotation(Inject.class); + + for (String dataSourceName : dataSourceNames) { + String dataSourceFieldName = "dataSource" + hashed(dataSourceName); + FieldCreator dataSourceField = classCreator.getFieldCreator(dataSourceFieldName, DataSource.class); + dataSourceField.setModifiers(ACCESS_PACKAGE_PROTECTED); + dataSourceField.addAnnotation(Inject.class); + dataSourceField.addAnnotation(annotatedWithNamed(dataSourceName)); + + String producerMethodName = "createFlywayForDataSource" + hashed(dataSourceName); + MethodCreator flywayProducerMethod = classCreator.getMethodCreator(producerMethodName, Flyway.class); + flywayProducerMethod.addAnnotation(Produces.class); + flywayProducerMethod.addAnnotation(Dependent.class); + flywayProducerMethod.addAnnotation(annotatedWithFlywayDatasource(dataSourceName)); + flywayProducerMethod.addAnnotation(annotatedWithNamed(FLYWAY_BEAN_NAME_PREFIX + dataSourceName)); + + flywayProducerMethod.returnValue( + flywayProducerMethod.invokeVirtualMethod( + createFlywayMethod(), + resultHandleFor(defaultProducerField, flywayProducerMethod), + resultHandleFor(dataSourceField, flywayProducerMethod), + flywayProducerMethod.load(dataSourceName))); + } + classCreator.close(); + } + + private void writeGeneratedBeanBuildItem(String name, byte[] data) { + generatedBean.produce(new GeneratedBeanBuildItem(name, data)); + } + + private static String hashed(String dataSourceName) { + return "_" + HashUtil.sha1(dataSourceName); + } + + private static MethodDescriptor createFlywayMethod() { + Class[] parameterTypes = { DataSource.class, String.class }; + return MethodDescriptor.ofMethod(FlywayProducer.class, "createFlyway", Flyway.class, parameterTypes); + } + + private static ResultHandle resultHandleFor(FieldCreator field, BytecodeCreator method) { + FieldDescriptor fieldDescriptor = field.getFieldDescriptor(); + return method.readInstanceField(fieldDescriptor, method.getThis()); + } + + private static AnnotationInstance annotatedWithNamed(String dataSourceName) { + return AnnotationInstance.create(DotNames.NAMED, null, + new AnnotationValue[] { AnnotationValue.createStringValue("value", dataSourceName) }); + } + + //Since is does not seem to be possible to generate the annotation "@Typed", + //because AnnotationValue.createArrayValue is not implemented yet (jandex, August 2019), + //the annotation "@FlywayDataSource" was introduced (in conformity with @DataSource). + private AnnotationInstance annotatedWithFlywayDatasource(String dataSourceName) { + return AnnotationInstance.create(DotName.createSimple(FlywayDataSource.class.getName()), null, + new AnnotationValue[] { AnnotationValue.createStringValue("value", dataSourceName) }); + } + + @Override + public String toString() { + return "FlywayDatasourceBeanGenerator [dataSourceNames=" + dataSourceNames + ", generatedBean=" + generatedBean + "]"; + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java index af692ba0c7ff6..ac194451a64c7 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java @@ -13,8 +13,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -24,9 +26,11 @@ import org.jboss.logging.Logger; import io.quarkus.agroal.deployment.DataSourceInitializedBuildItem; +import io.quarkus.agroal.deployment.DataSourceSchemaReadyBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -35,8 +39,10 @@ import io.quarkus.deployment.builditem.CapabilityBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; -import io.quarkus.flyway.runtime.FlywayBuildConfig; +import io.quarkus.deployment.recording.RecorderContext; +import io.quarkus.flyway.runtime.FlywayBuildTimeConfig; import io.quarkus.flyway.runtime.FlywayProducer; import io.quarkus.flyway.runtime.FlywayRecorder; import io.quarkus.flyway.runtime.FlywayRuntimeConfig; @@ -52,7 +58,7 @@ class FlywayProcessor { /** * Flyway build config */ - FlywayBuildConfig flywayBuildConfig; + FlywayBuildTimeConfig flywayBuildConfig; @BuildStep CapabilityBuildItem capability() { @@ -67,14 +73,20 @@ void build(BuildProducer additionalBeanProducer, BuildProducer containerListenerProducer, BuildProducer generatedResourceProducer, FlywayRecorder recorder, - DataSourceInitializedBuildItem dataSourceInitializedBuildItem) throws IOException, URISyntaxException { + DataSourceInitializedBuildItem dataSourceInitializedBuildItem, + BuildProducer generatedBeanBuildItem, + RecorderContext recorderContext) throws IOException, URISyntaxException { featureProducer.produce(new FeatureBuildItem(FeatureBuildItem.FLYWAY)); AdditionalBeanBuildItem unremovableProducer = AdditionalBeanBuildItem.unremovableOf(FlywayProducer.class); additionalBeanProducer.produce(unremovableProducer); - registerNativeImageResources(resourceProducer, generatedResourceProducer, flywayBuildConfig); + Collection dataSourceNames = DataSourceInitializedBuildItem.dataSourceNamesOf(dataSourceInitializedBuildItem); + new FlywayDatasourceBeanGenerator(dataSourceNames, generatedBeanBuildItem).createFlywayProducerBean(); + + registerNativeImageResources(resourceProducer, generatedResourceProducer, + discoverApplicationMigrations(getMigrationLocations(dataSourceInitializedBuildItem))); containerListenerProducer.produce( new BeanContainerListenerBuildItem(recorder.setFlywayBuildConfig(flywayBuildConfig))); @@ -89,20 +101,26 @@ void build(BuildProducer additionalBeanProducer, */ @Record(ExecutionTime.RUNTIME_INIT) @BuildStep - void configureRuntimeProperties(FlywayRecorder recorder, + ServiceStartBuildItem configureRuntimeProperties(FlywayRecorder recorder, FlywayRuntimeConfig flywayRuntimeConfig, BeanContainerBuildItem beanContainer, - DataSourceInitializedBuildItem dataSourceInitializedBuildItem) { + DataSourceInitializedBuildItem dataSourceInitializedBuildItem, + BuildProducer schemaReadyBuildItem) { recorder.configureFlywayProperties(flywayRuntimeConfig, beanContainer.getValue()); recorder.doStartActions(flywayRuntimeConfig, beanContainer.getValue()); + // once we are done running the migrations, we produce a build item indicating that the + // schema is "ready" + final Collection dataSourceNames = DataSourceInitializedBuildItem + .dataSourceNamesOf(dataSourceInitializedBuildItem); + schemaReadyBuildItem.produce(new DataSourceSchemaReadyBuildItem(dataSourceNames)); + return new ServiceStartBuildItem("flyway"); } private void registerNativeImageResources(BuildProducer resource, BuildProducer generatedResourceProducer, - FlywayBuildConfig flywayBuildConfig) + List applicationMigrations) throws IOException, URISyntaxException { final List nativeResources = new ArrayList<>(); - List applicationMigrations = discoverApplicationMigrations(flywayBuildConfig); nativeResources.addAll(applicationMigrations); // Store application migration in a generated resource that will be accessed later by the Quarkus-Flyway path scanner String resourcesList = applicationMigrations @@ -116,14 +134,30 @@ private void registerNativeImageResources(BuildProducer discoverApplicationMigrations(FlywayBuildConfig flywayBuildConfig) + /** + * Collects the configured migration locations for the default and all named DataSources. + *

+ * A {@link LinkedHashSet} is used to avoid duplications. + * + * @param dataSourceInitializedBuildItem {@link DataSourceInitializedBuildItem} + * @return {@link Collection} of {@link String}s + */ + private Collection getMigrationLocations(DataSourceInitializedBuildItem dataSourceInitializedBuildItem) { + Collection dataSourceNames = DataSourceInitializedBuildItem.dataSourceNamesOf(dataSourceInitializedBuildItem); + Collection migrationLocations = dataSourceNames.stream() + .map(flywayBuildConfig::getConfigForDataSourceName) + .flatMap(config -> config.locations.stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (DataSourceInitializedBuildItem.isDefaultDataSourcePresent(dataSourceInitializedBuildItem)) { + migrationLocations.addAll(flywayBuildConfig.defaultDataSource.locations); + } + return migrationLocations; + } + + private List discoverApplicationMigrations(Collection locations) throws IOException, URISyntaxException { List resources = new ArrayList<>(); try { - List locations = new ArrayList<>(flywayBuildConfig.locations); - if (locations.isEmpty()) { - locations.add("db/migration"); - } // Locations can be a comma separated list for (String location : locations) { // Strip any 'classpath:' protocol prefixes because they are assumed diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateNamedDataSourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateNamedDataSourceTest.java new file mode 100644 index 0000000000000..3e00aa79d0e67 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateNamedDataSourceTest.java @@ -0,0 +1,37 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionBaselineOnMigrateNamedDataSourceTest { + + @Inject + @FlywayDataSource("users") + Flyway flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("baseline-on-migrate-named-datasource.properties", "application.properties")); + + @Test + @DisplayName("Create history table correctly") + public void testFlywayInitialBaselineInfo() { + MigrationInfo baselineInfo = flyway.info().applied()[0]; + + assertEquals("0.0.1", baselineInfo.getVersion().getVersion()); + assertEquals("Initial description for test", baselineInfo.getDescription()); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateTest.java index 13984db28ee26..cd9e9b38dc31f 100644 --- a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateTest.java +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateTest.java @@ -15,7 +15,7 @@ import io.quarkus.test.QuarkusUnitTest; public class FlywayExtensionBaselineOnMigrateTest { - // Quarkus built object + @Inject Flyway flyway; diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAndMigrateAtStartTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAndMigrateAtStartTest.java new file mode 100644 index 0000000000000..a63a2f1d3feb7 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAndMigrateAtStartTest.java @@ -0,0 +1,49 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionCleanAndMigrateAtStartTest { + + @Inject + Flyway flyway; + + @Inject + AgroalDataSource defaultDataSource; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-and-migrate-at-start-config.properties", "application.properties")); + + @Test + @DisplayName("Clean and migrate at start correctly") + public void testFlywayConfigInjection() throws SQLException { + + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select * from fake_existing_tbl")) { + assertFalse(executeQuery.next(), "Table exists but is not empty"); + } + } + String currentVersion = flyway.info().current().getVersion().toString(); + assertEquals("1.0.0", currentVersion, "Expected to be 1.0.0 as migration runs at start"); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAtStartTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAtStartTest.java new file mode 100644 index 0000000000000..e7c33ee8d09c1 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAtStartTest.java @@ -0,0 +1,53 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.h2.jdbc.JdbcSQLException; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionCleanAtStartTest { + + @Inject + Flyway flyway; + + @Inject + AgroalDataSource defaultDataSource; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-at-start-config.properties", "application.properties")); + + @Test + @DisplayName("Clean at start correctly") + public void testFlywayConfigInjection() throws SQLException { + + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select * from fake_existing_tbl")) { + fail("fake_existing_tbl should not exist"); + } catch (JdbcSQLException e) { + // expected fake_existing_tbl does not exist + } + } + MigrationInfo current = flyway.info().current(); + assertNull(current, "Info is not null"); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigDefaultDataSourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigDefaultDataSourceTest.java new file mode 100644 index 0000000000000..b42729f107488 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigDefaultDataSourceTest.java @@ -0,0 +1,36 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigDefaultDataSourceTest { + + @Inject + Flyway flyway; + + @Inject + FlywayExtensionConfigFixture fixture; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-default-datasource.properties", "application.properties")); + + @Test + @DisplayName("Reads flyway configuration for default datasource correctly") + public void testFlywayConfigInjection() { + fixture.assertAllConfigurationSettings(flyway.getConfiguration(), ""); + assertFalse(fixture.migrateAtStart("")); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest.java new file mode 100644 index 0000000000000..4fee05bb4e9e3 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest.java @@ -0,0 +1,40 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +/** + * Assures, that Flyway can also be used without any configuration, + * provided, that at least a datasource is configured. + */ +public class FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest { + + @Inject + Flyway flyway; + + @Inject + FlywayExtensionConfigFixture fixture; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-default-datasource-without-flyway.properties", "application.properties")); + + @Test + @DisplayName("Reads predefined default flyway configuration for default datasource correctly") + public void testFlywayDefaultConfigInjection() { + fixture.assertDefaultConfigurationSettings(flyway.getConfiguration()); + assertFalse(fixture.migrateAtStart("")); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigEmptyTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigEmptyTest.java new file mode 100644 index 0000000000000..a7195077bdf8a --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigEmptyTest.java @@ -0,0 +1,38 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.UnsatisfiedResolutionException; +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +/** + * Flyway needs a datasource to work. + * This tests assures, that an error occurs, + * as soon as the default flyway configuration points to an missing default datasource. + */ +public class FlywayExtensionConfigEmptyTest { + + @Inject + Instance flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("config-empty.properties", "application.properties")); + + @Test + @DisplayName("Injecting (default) flyway should fail if there is no datasource configured") + public void testFlywayNotAvailableWithoutDataSource() { + assertThrows(UnsatisfiedResolutionException.class, flyway::get); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigFixture.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigFixture.java new file mode 100644 index 0000000000000..b40248186677c --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigFixture.java @@ -0,0 +1,166 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.junit.jupiter.api.Disabled; + +/** + * This fixture provides access to read the expected and the actual configuration of flyway. + * It also provides a method combining all assertions to be reused for multiple tests. + */ +@Disabled +@ApplicationScoped +public class FlywayExtensionConfigFixture { + + @Inject + Config config; + + public void assertAllConfigurationSettings(Configuration configuration, String dataSourceName) { + assertEquals(locations(configuration), locations(dataSourceName)); + assertEquals(sqlMigrationPrefix(configuration), sqlMigrationPrefix(dataSourceName)); + assertEquals(repeatableSqlMigrationPrefix(configuration), repeatableSqlMigrationPrefix(dataSourceName)); + assertEquals(tableName(configuration), tableName(dataSourceName)); + assertEquals(schemaNames(configuration), schemaNames(dataSourceName)); + + assertEquals(connectRetries(configuration), connectRetries(dataSourceName)); + + assertEquals(baselineOnMigrate(configuration), baselineOnMigrate(dataSourceName)); + assertEquals(baselineVersion(configuration), baselineVersion(dataSourceName)); + assertEquals(baselineDescription(configuration), baselineDescription(dataSourceName)); + } + + public void assertDefaultConfigurationSettings(Configuration configuration) { + FluentConfiguration defaultConfiguration = Flyway.configure(); + assertEquals(locations(configuration), locations(defaultConfiguration)); + assertEquals(sqlMigrationPrefix(configuration), sqlMigrationPrefix(defaultConfiguration)); + assertEquals(repeatableSqlMigrationPrefix(configuration), repeatableSqlMigrationPrefix(defaultConfiguration)); + assertEquals(tableName(configuration), tableName(defaultConfiguration)); + assertEquals(schemaNames(configuration), schemaNames(defaultConfiguration)); + + assertEquals(connectRetries(configuration), connectRetries(defaultConfiguration)); + + assertEquals(baselineOnMigrate(configuration), baselineOnMigrate(defaultConfiguration)); + assertEquals(baselineVersion(configuration), baselineVersion(defaultConfiguration)); + assertEquals(baselineDescription(configuration), baselineDescription(defaultConfiguration)); + } + + public int connectRetries(String datasourceName) { + return getIntValue("quarkus.flyway.%s.connect-retries", datasourceName); + } + + public int connectRetries(Configuration configuration) { + return configuration.getConnectRetries(); + } + + public String schemaNames(String datasourceName) { + return getStringValue("quarkus.flyway.%s.schemas", datasourceName); + } + + public String schemaNames(Configuration configuration) { + return Arrays.stream(configuration.getSchemas()).collect(Collectors.joining(",")); + } + + public String tableName(String datasourceName) { + return getStringValue("quarkus.flyway.%s.table", datasourceName); + } + + public String tableName(Configuration configuration) { + return configuration.getTable(); + } + + public String locations(String datasourceName) { + return getStringValue("quarkus.flyway.%s.locations", datasourceName); + } + + public String locations(Configuration configuration) { + return Arrays.stream(configuration.getLocations()).map(Location::getPath).collect(Collectors.joining(",")); + } + + public String sqlMigrationPrefix(String datasourceName) { + return getStringValue("quarkus.flyway.%s.sql-migration-prefix", datasourceName); + } + + public String sqlMigrationPrefix(Configuration configuration) { + return configuration.getSqlMigrationPrefix(); + } + + public String repeatableSqlMigrationPrefix(String datasourceName) { + return getStringValue("quarkus.flyway.%s.repeatable-sql-migration-prefix", datasourceName); + } + + public String repeatableSqlMigrationPrefix(Configuration configuration) { + return configuration.getRepeatableSqlMigrationPrefix(); + } + + public boolean baselineOnMigrate(String datasourceName) { + return getBooleanValue("quarkus.flyway.%s.baseline-on-migrate", datasourceName); + } + + public boolean baselineOnMigrate(Configuration configuration) { + return configuration.isBaselineOnMigrate(); + } + + public String baselineVersion(String datasourceName) { + return getStringValue("quarkus.flyway.%s.baseline-version", datasourceName); + } + + public String baselineVersion(Configuration configuration) { + return configuration.getBaselineVersion().getVersion(); + } + + public String baselineDescription(String datasourceName) { + return getStringValue("quarkus.flyway.%s.baseline-description", datasourceName); + } + + public String baselineDescription(Configuration configuration) { + return configuration.getBaselineDescription(); + } + + public boolean migrateAtStart(String datasourceName) { + return getBooleanValue("quarkus.flyway.migrate-at-start", datasourceName); + } + + private String getStringValue(String parameterName, String datasourceName) { + return getValue(parameterName, datasourceName, String.class); + } + + private int getIntValue(String parameterName, String datasourceName) { + return getValue(parameterName, datasourceName, Integer.class); + } + + private boolean getBooleanValue(String parameterName, String datasourceName) { + return getValue(parameterName, datasourceName, Boolean.class); + } + + private T getValue(String parameterName, String datasourceName, Class type) { + return getValue(parameterName, datasourceName, type, this::log); + } + + private T getValue(String parameterName, String datasourceName, Class type, Consumer logger) { + String propertyName = fillin(parameterName, datasourceName); + T propertyValue = config.getValue(propertyName, type); + logger.accept("Config property " + propertyName + " = " + propertyValue); + return propertyValue; + } + + private void log(String content) { + //activate for debugging + // System.out.println(content); + } + + private String fillin(String propertyName, String datasourceName) { + return String.format(propertyName, datasourceName).replace("..", "."); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMissingNamedDataSourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMissingNamedDataSourceTest.java new file mode 100644 index 0000000000000..d65f98b3eec7e --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMissingNamedDataSourceTest.java @@ -0,0 +1,39 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.UnsatisfiedResolutionException; +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Flyway needs a datasource to work. + * This tests assures, that an error occurs, as soon as a named flyway configuration points to an missing datasource. + */ +public class FlywayExtensionConfigMissingNamedDataSourceTest { + + @Inject + @FlywayDataSource("users") + Instance flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("config-for-missing-named-datasource.properties", "application.properties")); + + @Test + @DisplayName("Injecting flyway should fail if the named datasource is missing") + public void testFlywayNotAvailableWithoutDataSource() { + assertThrows(UnsatisfiedResolutionException.class, flyway::get); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMultiDataSourcesTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMultiDataSourcesTest.java new file mode 100644 index 0000000000000..4dd14e1da05f7 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMultiDataSourcesTest.java @@ -0,0 +1,74 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Test a full configuration with default and two named datasources plus their flyway settings. + */ +public class FlywayExtensionConfigMultiDataSourcesTest { + + @Inject + FlywayExtensionConfigFixture fixture; + + @Inject + Flyway flyway; + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @Inject + @FlywayDataSource("inventory") + Flyway flywayInventory; + + @Inject + @Named("flyway_inventory") + Flyway flywayNamedInventory; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-multiple-datasources.properties", "application.properties")); + + @Test + @DisplayName("Reads default flyway configuration for default datasource correctly") + public void testFlywayDefaultConfigInjection() { + fixture.assertAllConfigurationSettings(flyway.getConfiguration(), ""); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration for datasource named 'users' correctly") + public void testFlywayConfigNamedUsersInjection() { + fixture.assertAllConfigurationSettings(flywayUsers.getConfiguration(), "users"); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration for datasource named 'inventory' correctly") + public void testFlywayConfigNamedInventoryInjection() { + fixture.assertAllConfigurationSettings(flywayInventory.getConfiguration(), "inventory"); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration directly named 'inventory_flyway' correctly") + public void testFlywayConfigDirectlyNamedInventoryInjection() { + fixture.assertAllConfigurationSettings(flywayNamedInventory.getConfiguration(), "inventory"); + assertFalse(fixture.migrateAtStart("")); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest.java new file mode 100644 index 0000000000000..66118d4712055 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest.java @@ -0,0 +1,52 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Test a full configuration with default and two named datasources plus their flyway settings. + */ +public class FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest { + + @Inject + FlywayExtensionConfigFixture fixture; + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @Inject + @FlywayDataSource("inventory") + Flyway flywayInventory; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-multiple-datasources-without-default.properties", "application.properties")); + + @Test + @DisplayName("Reads flyway configuration for datasource named 'users' without default datasource correctly") + public void testFlywayConfigNamedUsersInjection() { + fixture.assertAllConfigurationSettings(flywayUsers.getConfiguration(), "users"); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration for datasource named 'inventory' without default datasource correctly") + public void testFlywayConfigNamedInventoryInjection() { + fixture.assertAllConfigurationSettings(flywayInventory.getConfiguration(), "inventory"); + assertFalse(fixture.migrateAtStart("")); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigNamedDataSourceWithoutDefaultTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigNamedDataSourceWithoutDefaultTest.java new file mode 100644 index 0000000000000..4effb9578dafe --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigNamedDataSourceWithoutDefaultTest.java @@ -0,0 +1,41 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Test a full configuration with default and two named datasources plus their flyway settings. + */ +public class FlywayExtensionConfigNamedDataSourceWithoutDefaultTest { + + @Inject + FlywayExtensionConfigFixture fixture; + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-named-datasource-without-default.properties", "application.properties")); + + @Test + @DisplayName("Reads flyway configuration for datasource named 'users' without default datasource correctly") + public void testFlywayConfigNamedUsersInjection() { + fixture.assertAllConfigurationSettings(flywayUsers.getConfiguration(), "users"); + assertFalse(fixture.migrateAtStart("")); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigNamedDataSourceWithoutFlywayTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigNamedDataSourceWithoutFlywayTest.java new file mode 100644 index 0000000000000..bcc66980f5c70 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigNamedDataSourceWithoutFlywayTest.java @@ -0,0 +1,42 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Assures, that flyway can also be used without any configuration, + * provided, that at least a named datasource is configured. + */ +public class FlywayExtensionConfigNamedDataSourceWithoutFlywayTest { + + @Inject + @FlywayDataSource("users") + Flyway flyway; + + @Inject + FlywayExtensionConfigFixture fixture; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-named-datasource-without-flyway.properties", "application.properties")); + + @Test + @DisplayName("Reads predefined default flyway configuration for named datasource correctly") + public void testFlywayDefaultConfigInjection() { + fixture.assertDefaultConfigurationSettings(flyway.getConfiguration()); + assertFalse(fixture.migrateAtStart("users")); + } +} \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionFullConfigTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionFullConfigTest.java deleted file mode 100644 index 3742f21c5ec2a..0000000000000 --- a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionFullConfigTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.quarkus.flyway.test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.flywaydb.core.Flyway; -import org.flywaydb.core.api.Location; -import org.flywaydb.core.api.configuration.Configuration; -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.quarkus.test.QuarkusUnitTest; - -public class FlywayExtensionFullConfigTest { - // Validation properties - @ConfigProperty(name = "quarkus.flyway.connect-retries") - int connectRetries; - @ConfigProperty(name = "quarkus.flyway.schemas") - List schemaNames; - @ConfigProperty(name = "quarkus.flyway.table") - String tableName; - @ConfigProperty(name = "quarkus.flyway.locations") - List locations; - @ConfigProperty(name = "quarkus.flyway.sql-migration-prefix") - String sqlMigrationPrefix; - @ConfigProperty(name = "quarkus.flyway.repeatable-sql-migration-prefix") - String repeatableSqlMigrationPrefix; - @ConfigProperty(name = "quarkus.flyway.baseline-on-migrate") - boolean baselineOnMigrate; - @ConfigProperty(name = "quarkus.flyway.baseline-version") - String baselineVersion; - @ConfigProperty(name = "quarkus.flyway.baseline-description") - String baselineDescription; - - // Quarkus built object - @Inject - Flyway flyway; - - @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addAsResource("full-config.properties", "application.properties")); - - @Test - @DisplayName("Reads flyway configuration correctly") - public void testFlywayConfigInjection() { - Configuration configuration = flyway.getConfiguration(); - - int locationsCount = locations.size(); - String joinedLocations = String.join(",", locations); - assertEquals(locationsCount, configuration.getLocations().length); - String configuredLocations = Arrays.stream(configuration.getLocations()).map(Location::getPath) - .collect(Collectors.joining(",")); - assertEquals(joinedLocations, configuredLocations); - - assertEquals(sqlMigrationPrefix, configuration.getSqlMigrationPrefix()); - assertEquals(repeatableSqlMigrationPrefix, configuration.getRepeatableSqlMigrationPrefix()); - - assertEquals(tableName, configuration.getTable()); - - int schemasCount = schemaNames.size(); - String joinedSchemas = String.join(",", schemaNames); - assertEquals(schemasCount, configuration.getSchemas().length); - String configuredNames = String.join(",", configuration.getSchemas()); - assertEquals(joinedSchemas, configuredNames); - - assertEquals(connectRetries, configuration.getConnectRetries()); - - assertEquals(baselineOnMigrate, configuration.isBaselineOnMigrate()); - assertEquals(baselineVersion, configuration.getBaselineVersion().getVersion()); - assertEquals(baselineDescription, configuration.getBaselineDescription()); - } -} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java new file mode 100644 index 0000000000000..50039cd1c8637 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java @@ -0,0 +1,39 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Same as {@link FlywayExtensionMigrateAtStartTest} for named datasources. + */ +public class FlywayExtensionMigrateAtStartNamedDataSourceTest { + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("migrate-at-start-config-named-datasource.properties", "application.properties")); + + @Test + @DisplayName("Migrates at start for datasource named 'users' correctly") + public void testFlywayConfigInjection() { + String currentVersion = flywayUsers.info().current().getVersion().toString(); + // Expected to be 1.0.0 as migration runs at start + assertEquals("1.0.0", currentVersion); + } +} diff --git a/extensions/flyway/deployment/src/test/resources/baseline-on-migrate-named-datasource.properties b/extensions/flyway/deployment/src/test/resources/baseline-on-migrate-named-datasource.properties new file mode 100644 index 0000000000000..b53de3c2144cc --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/baseline-on-migrate-named-datasource.properties @@ -0,0 +1,11 @@ +quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:test-quarkus-baseline-on-migrate;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'src/test/resources/h2-init-data.sql' +quarkus.datasource.users.driver=org.h2.Driver +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa + +# Flyway config properties +quarkus.flyway.users.migrate-at-start=true +quarkus.flyway.users.table=test_flyway_history +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=0.0.1 +quarkus.flyway.users.baseline-description=Initial description for test diff --git a/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-config.properties b/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-config.properties new file mode 100644 index 0000000000000..ab9eb76e6397d --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-config.properties @@ -0,0 +1,12 @@ +quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test-quarkus-clean-at-start;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'src/test/resources/h2-init-data.sql' +quarkus.datasource.driver=org.h2.Driver +quarkus.datasource.username=sa +quarkus.datasource.password=sa + +# Flyway config properties +quarkus.flyway.clean-at-start=true +quarkus.flyway.migrate-at-start=true +quarkus.flyway.table=test_flyway_history +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=0.0.1 +quarkus.flyway.baseline-description=Initial description for test diff --git a/extensions/flyway/deployment/src/test/resources/clean-at-start-config.properties b/extensions/flyway/deployment/src/test/resources/clean-at-start-config.properties new file mode 100644 index 0000000000000..0f86cfc9137f4 --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/clean-at-start-config.properties @@ -0,0 +1,12 @@ +quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test-quarkus-clean-at-start;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'src/test/resources/h2-init-data.sql' +quarkus.datasource.driver=org.h2.Driver +quarkus.datasource.username=sa +quarkus.datasource.password=sa + +# Flyway config properties +quarkus.flyway.clean-at-start=true +quarkus.flyway.migrate-at-start=false +quarkus.flyway.table=test_flyway_history +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=0.0.1 +quarkus.flyway.baseline-description=Initial description for test diff --git a/extensions/flyway/deployment/src/test/resources/config-empty.properties b/extensions/flyway/deployment/src/test/resources/config-empty.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/extensions/flyway/deployment/src/test/resources/config-for-default-datasource-without-flyway.properties b/extensions/flyway/deployment/src/test/resources/config-for-default-datasource-without-flyway.properties new file mode 100644 index 0000000000000..43a81aba22ef3 --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/config-for-default-datasource-without-flyway.properties @@ -0,0 +1,4 @@ +quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 +quarkus.datasource.driver=org.h2.Driver +quarkus.datasource.username=sa +quarkus.datasource.password=sa diff --git a/extensions/flyway/deployment/src/test/resources/full-config.properties b/extensions/flyway/deployment/src/test/resources/config-for-default-datasource.properties similarity index 96% rename from extensions/flyway/deployment/src/test/resources/full-config.properties rename to extensions/flyway/deployment/src/test/resources/config-for-default-datasource.properties index 82a222d07b9de..6abc5c4b9d5aa 100644 --- a/extensions/flyway/deployment/src/test/resources/full-config.properties +++ b/extensions/flyway/deployment/src/test/resources/config-for-default-datasource.properties @@ -1,7 +1,9 @@ +#default datasource quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 quarkus.datasource.driver=org.h2.Driver quarkus.datasource.username=sa quarkus.datasource.password=sa + # Flyway config properties quarkus.flyway.connect-retries=10 quarkus.flyway.schemas=TEST_SCHEMA diff --git a/extensions/flyway/deployment/src/test/resources/config-for-missing-named-datasource.properties b/extensions/flyway/deployment/src/test/resources/config-for-missing-named-datasource.properties new file mode 100644 index 0000000000000..e70bf9908bccb --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/config-for-missing-named-datasource.properties @@ -0,0 +1,19 @@ +# Datasource for "inventory" +quarkus.datasource.inventory.driver=org.h2.jdbcx.JdbcDataSource +quarkus.datasource.inventory.url=jdbc:h2:tcp://localhost/mem:inventory +quarkus.datasource.inventory.username=username2 +quarkus.datasource.inventory.min-size=2 +quarkus.datasource.inventory.max-size=12 +quarkus.datasource.inventory.xa=true + +# Flyway configuration for missing "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/resources/config-for-multiple-datasources-without-default.properties b/extensions/flyway/deployment/src/test/resources/config-for-multiple-datasources-without-default.properties new file mode 100644 index 0000000000000..a3829245ab5ba --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/config-for-multiple-datasources-without-default.properties @@ -0,0 +1,39 @@ +# Datasource for "users" +quarkus.datasource.users.driver=org.h2.Driver +quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:users +quarkus.datasource.users.username=username1 +quarkus.datasource.users.min-size=1 +quarkus.datasource.users.max-size=11 +quarkus.datasource.users.xa=false + +# Datasource for "inventory" +quarkus.datasource.inventory.driver=org.h2.jdbcx.JdbcDataSource +quarkus.datasource.inventory.url=jdbc:h2:tcp://localhost/mem:inventory +quarkus.datasource.inventory.username=username2 +quarkus.datasource.inventory.min-size=2 +quarkus.datasource.inventory.max-size=12 +quarkus.datasource.inventory.xa=true + +# Flyway configuration for "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description + +# Flyway configuration for "inventory" datasource +quarkus.flyway.inventory.connect-retries=12 +quarkus.flyway.inventory.schemas=INVENTORY_TEST_SCHEMA +quarkus.flyway.inventory.table=inventory_flyway_quarkus_history +quarkus.flyway.inventory.locations=db/inventory/location1,db/inventory/location2 +quarkus.flyway.inventory.sql-migration-prefix=I +quarkus.flyway.inventory.repeatable-sql-migration-prefix=N +quarkus.flyway.inventory.migrate-at-start=false +quarkus.flyway.inventory.baseline-on-migrate=true +quarkus.flyway.inventory.baseline-version=3.0.1 +quarkus.flyway.inventory.baseline-description=Inventory initial description \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/resources/config-for-multiple-datasources.properties b/extensions/flyway/deployment/src/test/resources/config-for-multiple-datasources.properties new file mode 100644 index 0000000000000..1fd2bef2416b0 --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/config-for-multiple-datasources.properties @@ -0,0 +1,57 @@ +# Datasource default +quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 +quarkus.datasource.driver=org.h2.Driver +quarkus.datasource.username=sa +quarkus.datasource.password=sa + +# Datasource for "users" +quarkus.datasource.users.driver=org.h2.Driver +quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:users +quarkus.datasource.users.username=username1 +quarkus.datasource.users.min-size=1 +quarkus.datasource.users.max-size=11 +quarkus.datasource.users.xa=false + +# Datasource for "inventory" +quarkus.datasource.inventory.driver=org.h2.jdbcx.JdbcDataSource +quarkus.datasource.inventory.url=jdbc:h2:tcp://localhost/mem:inventory +quarkus.datasource.inventory.username=username2 +quarkus.datasource.inventory.min-size=2 +quarkus.datasource.inventory.max-size=12 +quarkus.datasource.inventory.xa=true + +# Flyway configuration for default datasource +quarkus.flyway.connect-retries=10 +quarkus.flyway.schemas=TEST_SCHEMA +quarkus.flyway.table=flyway_quarkus_history +quarkus.flyway.locations=db/location1,db/location2 +quarkus.flyway.sql-migration-prefix=X +quarkus.flyway.repeatable-sql-migration-prefix=K +quarkus.flyway.migrate-at-start=false +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=2.0.1 +quarkus.flyway.baseline-description=Initial description + +# Flyway configuration for "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description + +# Flyway configuration for "inventory" datasource +quarkus.flyway.inventory.connect-retries=12 +quarkus.flyway.inventory.schemas=INVENTORY_TEST_SCHEMA +quarkus.flyway.inventory.table=inventory_flyway_quarkus_history +quarkus.flyway.inventory.locations=db/inventory/location1,db/inventory/location2 +quarkus.flyway.inventory.sql-migration-prefix=I +quarkus.flyway.inventory.repeatable-sql-migration-prefix=N +quarkus.flyway.inventory.migrate-at-start=false +quarkus.flyway.inventory.baseline-on-migrate=true +quarkus.flyway.inventory.baseline-version=3.0.1 +quarkus.flyway.inventory.baseline-description=Inventory initial description \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/resources/config-for-named-datasource-without-default.properties b/extensions/flyway/deployment/src/test/resources/config-for-named-datasource-without-default.properties new file mode 100644 index 0000000000000..3fd4b4629882b --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/config-for-named-datasource-without-default.properties @@ -0,0 +1,19 @@ +# Datasource for "users" +quarkus.datasource.users.driver=org.h2.Driver +quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:users +quarkus.datasource.users.username=username1 +quarkus.datasource.users.min-size=1 +quarkus.datasource.users.max-size=11 +quarkus.datasource.users.xa=false + +# Flyway configuration for "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/resources/config-for-named-datasource-without-flyway.properties b/extensions/flyway/deployment/src/test/resources/config-for-named-datasource-without-flyway.properties new file mode 100644 index 0000000000000..dc0ac10f57560 --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/config-for-named-datasource-without-flyway.properties @@ -0,0 +1,4 @@ +quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 +quarkus.datasource.users.driver=org.h2.Driver +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa diff --git a/extensions/flyway/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties b/extensions/flyway/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties new file mode 100644 index 0000000000000..8eddd6a9413de --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties @@ -0,0 +1,6 @@ +quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start-users;DB_CLOSE_DELAY=-1 +quarkus.datasource.users.driver=org.h2.Driver +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa +# Flyway config properties for datasource named users +quarkus.flyway.users.migrate-at-start=true diff --git a/extensions/flyway/runtime/pom.xml b/extensions/flyway/runtime/pom.xml index f1804ae019a7f..4ce1b1c881d6d 100644 --- a/extensions/flyway/runtime/pom.xml +++ b/extensions/flyway/runtime/pom.xml @@ -33,6 +33,13 @@ com.oracle.substratevm svm + + + + io.quarkus + quarkus-junit5-internal + test + diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/FlywayDataSource.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/FlywayDataSource.java new file mode 100644 index 0000000000000..ba4229d4e67bf --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/FlywayDataSource.java @@ -0,0 +1,60 @@ +package io.quarkus.flyway; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Named; +import javax.inject.Qualifier; + +/** + * Qualifier used to specify which datasource will be used and therefore which Flyway instance will be injected. + *

+ * Flyway instances can also be qualified by name using @{@link Named}. + * The name is the datasource name prefixed by "flyway_". + */ +@Target({ METHOD, FIELD, PARAMETER, TYPE }) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface FlywayDataSource { + + String value(); + + /** + * Supports inline instantiation of the {@link FlywayDataSource} qualifier. + */ + public static final class FlywayDataSourceLiteral extends AnnotationLiteral implements FlywayDataSource { + + public static final FlywayDataSourceLiteral INSTANCE = of(""); + + private static final long serialVersionUID = 1L; + + private final String value; + + public static FlywayDataSourceLiteral of(String value) { + return new FlywayDataSourceLiteral(value); + } + + @Override + public String value() { + return value; + } + + private FlywayDataSourceLiteral(String value) { + this.value = value; + } + + @Override + public String toString() { + return "FlywayDataSourceLiteral [value=" + value + "]"; + } + } +} \ No newline at end of file diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildConfig.java deleted file mode 100644 index 8c2d21f0c05ec..0000000000000 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.quarkus.flyway.runtime; - -import java.util.List; - -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; - -@ConfigRoot(name = "flyway", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) -public final class FlywayBuildConfig { - /** - * Comma-separated list of locations to scan recursively for migrations. The location type is determined by its prefix. - * Unprefixed locations or locations starting with classpath: point to a package on the classpath and may contain both SQL - * and Java-based migrations. - * Locations starting with filesystem: point to a directory on the filesystem, may only contain SQL migrations and are only - * scanned recursively down non-hidden directories. - */ - @ConfigItem - public List locations; -} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildTimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildTimeConfig.java new file mode 100644 index 0000000000000..8e856d661872f --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildTimeConfig.java @@ -0,0 +1,37 @@ +package io.quarkus.flyway.runtime; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "flyway", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public final class FlywayBuildTimeConfig { + + public static final FlywayBuildTimeConfig defaultConfig() { + FlywayBuildTimeConfig defaultConfig = new FlywayBuildTimeConfig(); + defaultConfig.defaultDataSource = FlywayDataSourceBuildTimeConfig.defaultConfig(); + return defaultConfig; + } + + /** + * Gets the {@link FlywayDataSourceBuildTimeConfig} for the given datasource name. + */ + public FlywayDataSourceBuildTimeConfig getConfigForDataSourceName(String dataSourceName) { + return namedDataSources.getOrDefault(dataSourceName, FlywayDataSourceBuildTimeConfig.defaultConfig()); + } + + /** + * Flyway configuration for the default datasource. + */ + @ConfigItem(name = ConfigItem.PARENT) + public FlywayDataSourceBuildTimeConfig defaultDataSource; + + /** + * Flyway configurations for named datasources. + */ + @ConfigItem(name = ConfigItem.PARENT) + public Map namedDataSources = Collections.emptyMap(); +} \ No newline at end of file diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java new file mode 100644 index 0000000000000..b9557c88ac2a6 --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java @@ -0,0 +1,36 @@ +package io.quarkus.flyway.runtime; + +import javax.sql.DataSource; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.configuration.FluentConfiguration; + +class FlywayCreator { + + private static final String[] EMPTY_ARRAY = new String[0]; + + private final FlywayDataSourceRuntimeConfig flywayRuntimeConfig; + private final FlywayDataSourceBuildTimeConfig flywayBuildTimeConfig; + + public FlywayCreator(FlywayDataSourceRuntimeConfig flywayRuntimeConfig, + FlywayDataSourceBuildTimeConfig flywayBuildTimeConfig) { + this.flywayRuntimeConfig = flywayRuntimeConfig; + this.flywayBuildTimeConfig = flywayBuildTimeConfig; + } + + public Flyway createFlyway(DataSource dataSource) { + FluentConfiguration configure = Flyway.configure(); + configure.dataSource(dataSource); + flywayRuntimeConfig.connectRetries.ifPresent(configure::connectRetries); + flywayRuntimeConfig.schemas.ifPresent(list -> configure.schemas(list.toArray(EMPTY_ARRAY))); + flywayRuntimeConfig.table.ifPresent(configure::table); + configure.locations(flywayBuildTimeConfig.locations.toArray(EMPTY_ARRAY)); + flywayRuntimeConfig.sqlMigrationPrefix.ifPresent(configure::sqlMigrationPrefix); + flywayRuntimeConfig.repeatableSqlMigrationPrefix.ifPresent(configure::repeatableSqlMigrationPrefix); + configure.baselineOnMigrate(flywayRuntimeConfig.baselineOnMigrate); + configure.validateOnMigrate(flywayRuntimeConfig.validateOnMigrate); + flywayRuntimeConfig.baselineVersion.ifPresent(configure::baselineVersion); + flywayRuntimeConfig.baselineDescription.ifPresent(configure::baselineDescription); + return configure.load(); + } +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceBuildTimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceBuildTimeConfig.java new file mode 100644 index 0000000000000..ee3e41f382c38 --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceBuildTimeConfig.java @@ -0,0 +1,36 @@ +package io.quarkus.flyway.runtime; + +import java.util.Arrays; +import java.util.List; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public final class FlywayDataSourceBuildTimeConfig { + + private static final String DEFAULT_LOCATION = "db/migration"; + + /** + * Creates a {@link FlywayDataSourceBuildTimeConfig} with default settings. + * + * @return {@link FlywayDataSourceBuildTimeConfig} + */ + public static final FlywayDataSourceBuildTimeConfig defaultConfig() { + FlywayDataSourceBuildTimeConfig defaultConfig = new FlywayDataSourceBuildTimeConfig(); + defaultConfig.locations = Arrays.asList(DEFAULT_LOCATION); + return defaultConfig; + } + + /** + * Comma-separated list of locations to scan recursively for migrations. The location type is determined by its prefix. + *

+ * Unprefixed locations or locations starting with classpath: point to a package on the classpath and may contain both SQL + * and Java-based migrations. + *

+ * Locations starting with filesystem: point to a directory on the filesystem, may only contain SQL migrations and are only + * scanned recursively down non-hidden directories. + */ + @ConfigItem(defaultValue = DEFAULT_LOCATION) + public List locations; +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java new file mode 100644 index 0000000000000..4e240be1a9d30 --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java @@ -0,0 +1,102 @@ +package io.quarkus.flyway.runtime; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public final class FlywayDataSourceRuntimeConfig { + + /** + * Creates a {@link FlywayDataSourceRuntimeConfig} with default settings. + * + * @return {@link FlywayDataSourceRuntimeConfig} + */ + public static final FlywayDataSourceRuntimeConfig defaultConfig() { + return new FlywayDataSourceRuntimeConfig(); + } + + /** + * The maximum number of retries when attempting to connect to the database. After each failed attempt, Flyway will wait 1 + * second before attempting to connect again, up to the maximum number of times specified by connectRetries. + */ + @ConfigItem + public OptionalInt connectRetries = OptionalInt.empty(); + + /** + * Comma-separated case-sensitive list of schemas managed by Flyway. + * The first schema in the list will be automatically set as the default one during the migration. + * It will also be the one containing the schema history table. + */ + @ConfigItem + public Optional> schemas = Optional.empty(); + + /** + * The name of Flyway's schema history table. + * By default (single-schema mode) the schema history table is placed in the default schema for the connection provided by + * the datasource. + * When the flyway.schemas property is set (multi-schema mode), the schema history table is placed in the first schema of + * the list. + */ + @ConfigItem + public Optional table = Optional.empty(); + + /** + * The file name prefix for versioned SQL migrations. + * + * Versioned SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , which using + * the defaults translates to V1.1__My_description.sql + */ + @ConfigItem + public Optional sqlMigrationPrefix = Optional.empty(); + + /** + * The file name prefix for repeatable SQL migrations. + * + * Repeatable SQL migrations have the following file name structure: prefixSeparatorDESCRIPTIONsuffix , which using the + * defaults translates to R__My_description.sql + */ + @ConfigItem + public Optional repeatableSqlMigrationPrefix = Optional.empty(); + + /** + * true to execute Flyway clean command automatically when the application starts, false otherwise. + * + */ + @ConfigItem + public boolean cleanAtStart; + + /** + * true to execute Flyway automatically when the application starts, false otherwise. + * + */ + @ConfigItem + public boolean migrateAtStart; + + /** + * Enable the creation of the history table if it does not exist already. + */ + @ConfigItem + public boolean baselineOnMigrate; + + /** + * The initial baseline version. + */ + @ConfigItem + public Optional baselineVersion = Optional.empty(); + + /** + * The description to tag an existing schema with when executing baseline. + */ + @ConfigItem + public Optional baselineDescription = Optional.empty(); + + /** + * Whether to automatically call validate when performing a migration. + */ + @ConfigItem + public boolean validateOnMigrate = true; +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayProducer.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayProducer.java index 1c1c6637e16fa..ac9f9c7dd6c21 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayProducer.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayProducer.java @@ -1,62 +1,64 @@ package io.quarkus.flyway.runtime; -import java.util.List; -import java.util.stream.Collectors; - import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.Instance; import javax.enterprise.inject.Produces; import javax.inject.Inject; +import javax.sql.DataSource; import org.flywaydb.core.Flyway; -import org.flywaydb.core.api.configuration.FluentConfiguration; - -import io.agroal.api.AgroalDataSource; @ApplicationScoped public class FlywayProducer { + private static final String ERROR_NOT_READY = "The Flyway settings are not ready to be consumed: the %s configuration has not been injected yet"; + @Inject - AgroalDataSource dataSource; + @Default + Instance defaultDataSource; + private FlywayRuntimeConfig flywayRuntimeConfig; - private FlywayBuildConfig flywayBuildConfig; + private FlywayBuildTimeConfig flywayBuildConfig; @Produces @Dependent + @Default public Flyway produceFlyway() { - FluentConfiguration configure = Flyway.configure(); - configure.dataSource(dataSource); - flywayRuntimeConfig.connectRetries.ifPresent(configure::connectRetries); - List notEmptySchemas = filterBlanks(flywayRuntimeConfig.schemas); - if (!notEmptySchemas.isEmpty()) { - configure.schemas(notEmptySchemas.toArray(new String[0])); - } - flywayRuntimeConfig.table.ifPresent(configure::table); - List notEmptyLocations = filterBlanks(flywayBuildConfig.locations); - if (!notEmptyLocations.isEmpty()) { - configure.locations(notEmptyLocations.toArray(new String[0])); - } - flywayRuntimeConfig.sqlMigrationPrefix.ifPresent(configure::sqlMigrationPrefix); - flywayRuntimeConfig.repeatableSqlMigrationPrefix.ifPresent(configure::repeatableSqlMigrationPrefix); + return createDefaultFlyway(defaultDataSource.get()); + } - configure.baselineOnMigrate(flywayRuntimeConfig.baselineOnMigrate); - flywayRuntimeConfig.baselineVersion.ifPresent(configure::baselineVersion); - flywayRuntimeConfig.baselineDescription.ifPresent(configure::baselineDescription); + public void setFlywayRuntimeConfig(FlywayRuntimeConfig flywayRuntimeConfig) { + this.flywayRuntimeConfig = flywayRuntimeConfig; + } - return configure.load(); + public void setFlywayBuildConfig(FlywayBuildTimeConfig flywayBuildConfig) { + this.flywayBuildConfig = flywayBuildConfig; } - // NOTE: Have to do this filtering because SmallRye config was injecting an empty string in the list somehow! - // TODO: remove this when https://github.com/quarkusio/quarkus/issues/2288 is fixed - private List filterBlanks(List values) { - return values.stream().filter(it -> it != null && !"".equals(it)) - .collect(Collectors.toList()); + private Flyway createDefaultFlyway(DataSource dataSource) { + return new FlywayCreator(getFlywayRuntimeConfig().defaultDataSource, getFlywayBuildConfig().defaultDataSource) + .createFlyway(dataSource); } - public void setFlywayRuntimeConfig(FlywayRuntimeConfig flywayRuntimeConfig) { - this.flywayRuntimeConfig = flywayRuntimeConfig; + public Flyway createFlyway(DataSource dataSource, String dataSourceName) { + return new FlywayCreator(getFlywayRuntimeConfig().getConfigForDataSourceName(dataSourceName), + getFlywayBuildConfig().getConfigForDataSourceName(dataSourceName)) + .createFlyway(dataSource); } - public void setFlywayBuildConfig(FlywayBuildConfig flywayBuildConfig) { - this.flywayBuildConfig = flywayBuildConfig; + private FlywayRuntimeConfig getFlywayRuntimeConfig() { + return failIfNotReady(flywayRuntimeConfig, "runtime"); + } + + private FlywayBuildTimeConfig getFlywayBuildConfig() { + return failIfNotReady(flywayBuildConfig, "build"); + } + + private static T failIfNotReady(T config, String name) { + if (config == null) { + throw new IllegalStateException(String.format(ERROR_NOT_READY, name)); + } + return config; } -} +} \ No newline at end of file diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java index c71e570d503c5..b6167ec37d340 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java @@ -1,15 +1,22 @@ package io.quarkus.flyway.runtime; +import java.lang.annotation.Annotation; +import java.util.Map.Entry; + +import javax.enterprise.inject.Default; +import javax.enterprise.util.AnnotationLiteral; + import org.flywaydb.core.Flyway; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.arc.runtime.BeanContainerListener; +import io.quarkus.flyway.FlywayDataSource; import io.quarkus.runtime.annotations.Recorder; @Recorder public class FlywayRecorder { - public BeanContainerListener setFlywayBuildConfig(FlywayBuildConfig flywayBuildConfig) { + public BeanContainerListener setFlywayBuildConfig(FlywayBuildTimeConfig flywayBuildConfig) { return beanContainer -> { FlywayProducer producer = beanContainer.instance(FlywayProducer.class); producer.setFlywayBuildConfig(flywayBuildConfig); @@ -21,9 +28,27 @@ public void configureFlywayProperties(FlywayRuntimeConfig flywayRuntimeConfig, B } public void doStartActions(FlywayRuntimeConfig config, BeanContainer container) { - if (config.migrateAtStart) { - Flyway flyway = container.instance(Flyway.class); - flyway.migrate(); + if (config.defaultDataSource.cleanAtStart) { + clean(container, Default.Literal.INSTANCE); + } + if (config.defaultDataSource.migrateAtStart) { + migrate(container, Default.Literal.INSTANCE); + } + for (Entry configPerDataSource : config.namedDataSources.entrySet()) { + if (configPerDataSource.getValue().cleanAtStart) { + clean(container, FlywayDataSource.FlywayDataSourceLiteral.of(configPerDataSource.getKey())); + } + if (configPerDataSource.getValue().migrateAtStart) { + migrate(container, FlywayDataSource.FlywayDataSourceLiteral.of(configPerDataSource.getKey())); + } } } + + private void clean(BeanContainer container, AnnotationLiteral qualifier) { + container.instance(Flyway.class, qualifier).clean(); + } + + private void migrate(BeanContainer container, AnnotationLiteral qualifier) { + container.instance(Flyway.class, qualifier).migrate(); + } } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java index b2a17c8260225..637761d1be00f 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java @@ -1,8 +1,7 @@ package io.quarkus.flyway.runtime; -import java.util.List; -import java.util.Optional; -import java.util.OptionalInt; +import java.util.Collections; +import java.util.Map; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; @@ -10,69 +9,27 @@ @ConfigRoot(name = "flyway", phase = ConfigPhase.RUN_TIME) public final class FlywayRuntimeConfig { - /** - * The maximum number of retries when attempting to connect to the database. After each failed attempt, Flyway will wait 1 - * second before attempting to connect again, up to the maximum number of times specified by connectRetries. - */ - @ConfigItem - public OptionalInt connectRetries; - /** - * Comma-separated case-sensitive list of schemas managed by Flyway. - * The first schema in the list will be automatically set as the default one during the migration. - * It will also be the one containing the schema history table. - */ - @ConfigItem - public List schemas; - /** - * The name of Flyway's schema history table. - * By default (single-schema mode) the schema history table is placed in the default schema for the connection provided by - * the datasource. - * When the flyway.schemas property is set (multi-schema mode), the schema history table is placed in the first schema of - * the list. - */ - @ConfigItem - public Optional table; - - /** - * The file name prefix for versioned SQL migrations. - * - * Versioned SQL migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix , which using - * the defaults translates to V1.1__My_description.sql - */ - @ConfigItem - public Optional sqlMigrationPrefix; - /** - * The file name prefix for repeatable SQL migrations. - * - * Repeatable SQL migrations have the following file name structure: prefixSeparatorDESCRIPTIONsuffix , which using the - * defaults translates to R__My_description.sql - */ - @ConfigItem - public Optional repeatableSqlMigrationPrefix; - - /** - * true to execute Flyway automatically when the application starts, false otherwise. - * - */ - @ConfigItem - public boolean migrateAtStart; + public static final FlywayRuntimeConfig defaultConfig() { + return new FlywayRuntimeConfig(); + } /** - * Enable the creation of the history table if it does not exist already. + * Gets the {@link FlywayDataSourceRuntimeConfig} for the given datasource name. */ - @ConfigItem - public boolean baselineOnMigrate; + public FlywayDataSourceRuntimeConfig getConfigForDataSourceName(String dataSourceName) { + return namedDataSources.getOrDefault(dataSourceName, FlywayDataSourceRuntimeConfig.defaultConfig()); + } /** - * The initial baseline version. + * Flyway configuration for the default datasource. */ - @ConfigItem - public Optional baselineVersion; + @ConfigItem(name = ConfigItem.PARENT) + public FlywayDataSourceRuntimeConfig defaultDataSource = FlywayDataSourceRuntimeConfig.defaultConfig(); /** - * The description to tag an existing schema with when executing baseline. + * Flyway configurations for named datasources. */ - @ConfigItem - public Optional baselineDescription; -} + @ConfigItem(name = ConfigItem.PARENT) + public Map namedDataSources = Collections.emptyMap(); +} \ No newline at end of file diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/graal/ScannerSubstitutions.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/graal/ScannerSubstitutions.java index f952abf41158b..46ddd977c8b0b 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/graal/ScannerSubstitutions.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/graal/ScannerSubstitutions.java @@ -7,6 +7,7 @@ import org.flywaydb.core.api.Location; import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.scanner.ResourceNameCache; import org.flywaydb.core.internal.scanner.classpath.ResourceAndClassScanner; import com.oracle.svm.core.annotate.Alias; @@ -21,6 +22,7 @@ public final class ScannerSubstitutions { @Alias private List resources = new ArrayList<>(); + @Alias private List> classes = new ArrayList<>(); @@ -33,7 +35,7 @@ public final class ScannerSubstitutions { */ @Substitute public ScannerSubstitutions(Class implementedInterface, Collection locations, ClassLoader classLoader, - Charset encoding) { + Charset encoding, ResourceNameCache resourceNameCache) { ResourceAndClassScanner quarkusScanner = new QuarkusPathLocationScanner(); resources.addAll(quarkusScanner.scanForResources()); classes.addAll(quarkusScanner.scanForClasses()); diff --git a/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayCreatorTest.java b/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayCreatorTest.java new file mode 100644 index 0000000000000..3731e1b13d233 --- /dev/null +++ b/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayCreatorTest.java @@ -0,0 +1,192 @@ +package io.quarkus.flyway.runtime; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.configuration.Configuration; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class FlywayCreatorTest { + + private FlywayDataSourceRuntimeConfig runtimeConfig = FlywayDataSourceRuntimeConfig.defaultConfig(); + private FlywayDataSourceBuildTimeConfig buildConfig = FlywayDataSourceBuildTimeConfig.defaultConfig(); + private Configuration defaultConfig = Flyway.configure().load().getConfiguration(); + + /** + * class under test. + */ + private FlywayCreator creator; + + @Test + @DisplayName("locations default matches flyway default") + void testLocationsDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(pathList(defaultConfig.getLocations()), pathList(createdFlywayConfig().getLocations())); + } + + @Test + @DisplayName("locations carried over from configuration") + void testLocationsOverridden() { + buildConfig.locations = Arrays.asList("db/migrations", "db/something"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(buildConfig.locations, pathList(createdFlywayConfig().getLocations())); + } + + @Test + @DisplayName("not configured locations replaced by default") + void testNotPresentLocationsOverridden() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(pathList(defaultConfig.getLocations()), pathList(createdFlywayConfig().getLocations())); + } + + @Test + @DisplayName("baseline description default matches flyway default") + void testBaselineDescriptionDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getBaselineDescription(), createdFlywayConfig().getBaselineDescription()); + } + + @Test + @DisplayName("baseline description carried over from configuration") + void testBaselineDescriptionOverridden() { + runtimeConfig.baselineDescription = Optional.of("baselineDescription"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.baselineDescription.get(), createdFlywayConfig().getBaselineDescription()); + } + + @Test + @DisplayName("baseline version default matches flyway default") + void testBaselineVersionDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getBaselineVersion(), createdFlywayConfig().getBaselineVersion()); + } + + @Test + @DisplayName("baseline version carried over from configuration") + void testBaselineVersionOverridden() { + runtimeConfig.baselineVersion = Optional.of("0.1.2"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.baselineVersion.get(), createdFlywayConfig().getBaselineVersion().getVersion()); + } + + @Test + @DisplayName("connection retries default matches flyway default") + void testConnectionRetriesDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getConnectRetries(), createdFlywayConfig().getConnectRetries()); + } + + @Test + @DisplayName("connection retries carried over from configuration") + void testConnectionRetriesOverridden() { + runtimeConfig.connectRetries = OptionalInt.of(12); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.connectRetries.getAsInt(), createdFlywayConfig().getConnectRetries()); + } + + @Test + @DisplayName("repeatable SQL migration prefix default matches flyway default") + void testRepeatableSqlMigrationPrefixDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getRepeatableSqlMigrationPrefix(), createdFlywayConfig().getRepeatableSqlMigrationPrefix()); + } + + @Test + @DisplayName("repeatable SQL migration prefix carried over from configuration") + void testRepeatableSqlMigrationPrefixOverridden() { + runtimeConfig.repeatableSqlMigrationPrefix = Optional.of("A"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.repeatableSqlMigrationPrefix.get(), createdFlywayConfig().getRepeatableSqlMigrationPrefix()); + } + + @Test + @DisplayName("schemas default matches flyway default") + void testSchemasDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(asList(defaultConfig.getSchemas()), asList(createdFlywayConfig().getSchemas())); + } + + @Test + @DisplayName("schemas carried over from configuration") + void testSchemasOverridden() { + runtimeConfig.schemas = Optional.of(Arrays.asList("TEST_SCHEMA_1", "TEST_SCHEMA_2")); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.schemas.get(), asList(createdFlywayConfig().getSchemas())); + } + + @Test + @DisplayName("SQL migration prefix default matches flyway default") + void testSqlMigrationPrefixDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getSqlMigrationPrefix(), createdFlywayConfig().getSqlMigrationPrefix()); + } + + @Test + @DisplayName("SQL migration prefix carried over from configuration") + void testSqlMigrationPrefixOverridden() { + runtimeConfig.sqlMigrationPrefix = Optional.of("M"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.sqlMigrationPrefix.get(), createdFlywayConfig().getSqlMigrationPrefix()); + } + + @Test + @DisplayName("table default matches flyway default") + void testTableDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getTable(), createdFlywayConfig().getTable()); + } + + @Test + @DisplayName("table carried over from configuration") + void testTableOverridden() { + runtimeConfig.table = Optional.of("flyway_history_test_table"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.table.get(), createdFlywayConfig().getTable()); + } + + @Test + @DisplayName("validate on migrate default matches to true") + void testValidateOnMigrate() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.validateOnMigrate, createdFlywayConfig().isValidateOnMigrate()); + assertEquals(runtimeConfig.validateOnMigrate, true); + } + + @ParameterizedTest + @MethodSource("validateOnMigrateOverwritten") + @DisplayName("validate on migrate overwritten in configuration") + void testValidateOnMigrateOverwritten(final boolean input, final boolean expected) { + runtimeConfig.validateOnMigrate = input; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(createdFlywayConfig().isValidateOnMigrate(), expected); + assertEquals(runtimeConfig.validateOnMigrate, expected); + } + + private static List pathList(Location[] locations) { + return Stream.of(locations).map(Location::getPath).collect(Collectors.toList()); + } + + private Configuration createdFlywayConfig() { + return creator.createFlyway(null).getConfiguration(); + } + + private static Stream validateOnMigrateOverwritten() { + return Stream. builder() + .add(Arguments.arguments(false, false)) + .add(Arguments.arguments(true, true)) + .build(); + } +} diff --git a/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayProducerTest.java b/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayProducerTest.java new file mode 100644 index 0000000000000..c62912c39b23d --- /dev/null +++ b/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayProducerTest.java @@ -0,0 +1,46 @@ +package io.quarkus.flyway.runtime; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FlywayProducerTest { + + private static final String DEFAULT_DATASOURCE = ""; + private FlywayBuildTimeConfig buildDataSourceConfig = FlywayBuildTimeConfig.defaultConfig(); + private FlywayRuntimeConfig runtimeDataSourceConfig = FlywayRuntimeConfig.defaultConfig(); + + /** + * class under test. + */ + private FlywayProducer flywayProducer = new FlywayProducer(); + + @BeforeEach + void beforeEach() { + flywayProducer.setFlywayBuildConfig(buildDataSourceConfig); + flywayProducer.setFlywayRuntimeConfig(runtimeDataSourceConfig); + } + + @Test + @DisplayName("flyway can be created successfully") + void testCreatesFlywaySuccessfully() { + assertNotNull(flywayProducer.createFlyway(null, DEFAULT_DATASOURCE)); + } + + @Test + @DisplayName("fail on missing build configuration") + void testMissingBuildConfig() { + flywayProducer.setFlywayBuildConfig(null); + assertThrows(IllegalStateException.class, () -> flywayProducer.createFlyway(null, DEFAULT_DATASOURCE)); + } + + @Test + @DisplayName("fail on missing runtime configuration") + void testMissingRuntimeConfig() { + flywayProducer.setFlywayRuntimeConfig(null); + assertThrows(IllegalStateException.class, () -> flywayProducer.createFlyway(null, DEFAULT_DATASOURCE)); + } +} \ No newline at end of file diff --git a/extensions/hibernate-orm/deployment/pom.xml b/extensions/hibernate-orm/deployment/pom.xml index 5b6294a216614..5aa35eb56f053 100644 --- a/extensions/hibernate-orm/deployment/pom.xml +++ b/extensions/hibernate-orm/deployment/pom.xml @@ -33,7 +33,6 @@ io.quarkus quarkus-arc-deployment - io.quarkus quarkus-junit5-internal @@ -71,22 +70,6 @@ hibernate-envers test - - org.glassfish.jaxb - jaxb-runtime - test - - - jakarta.xml.bind - jakarta.xml.bind-api - - - - - org.jboss.spec.javax.xml.bind - jboss-jaxb-api_2.3_spec - test - diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateEntityEnhancer.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateEntityEnhancer.java index 6d618920e57b8..f45c3d7d9a3e6 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateEntityEnhancer.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateEntityEnhancer.java @@ -4,6 +4,7 @@ import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext; import org.hibernate.bytecode.enhance.spi.Enhancer; +import org.hibernate.bytecode.enhance.spi.UnloadedField; import org.hibernate.bytecode.spi.BytecodeProvider; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -46,6 +47,15 @@ public HibernateEnhancingClassVisitor(String className, ClassVisitor outputClass //note that as getLoadingClassLoader is resolved immediately this can't be created until transform time DefaultEnhancementContext enhancementContext = new DefaultEnhancementContext() { + + @Override + public boolean doBiDirectionalAssociationManagement(final UnloadedField field) { + //Don't enable automatic association management as it's often too surprising. + //Also, there's several cases in which its semantics are of unspecified, + //such as what should happen when dealing with ordered collections. + return false; + } + @Override public ClassLoader getLoadingClassLoader() { return Thread.currentThread().getContextClassLoader(); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java index 6dffbebcadb90..5020b78572640 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfig.java @@ -85,6 +85,22 @@ public class HibernateOrmConfig { @ConfigItem(defaultValue = "-1") public int batchFetchSize; + /** + * Pluggable strategy contract for applying physical naming rules for database object names. + * + * Class name of the Hibernate PhysicalNamingStrategy implementation + */ + @ConfigItem + Optional physicalNamingStrategy; + + /** + * Pluggable strategy for applying implicit naming rules when an explicit name is not given. + * + * Class name of the Hibernate ImplicitNamingStrategy implementation + */ + @ConfigItem + Optional implicitNamingStrategy; + /** * Query related configuration. */ @@ -199,10 +215,17 @@ public static class HibernateOrmConfigDatabase { @ConfigItem public Optional charset; + /** + * Whether Hibernate should quote all identifiers. + */ + @ConfigItem(defaultValue = "false") + public boolean globallyQuotedIdentifiers; + public boolean isAnyPropertySet() { return !"none".equals(generation) || defaultCatalog.isPresent() || defaultSchema.isPresent() || generationHaltOnError - || charset.isPresent(); + || charset.isPresent() + || globallyQuotedIdentifiers; } } diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index b81646a93b48f..b95a0e0f1333e 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -4,8 +4,8 @@ import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -45,6 +45,7 @@ import io.quarkus.agroal.deployment.DataSourceDriverBuildItem; import io.quarkus.agroal.deployment.DataSourceInitializedBuildItem; +import io.quarkus.agroal.deployment.DataSourceSchemaReadyBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; @@ -263,13 +264,13 @@ void handleNativeImageImportSql(BuildProducer reso @BuildStep void setupResourceInjection(BuildProducer resourceAnnotations, BuildProducer resources, - JpaEntitiesBuildItem jpaEntities, List nonJpaModels) throws UnsupportedEncodingException { + JpaEntitiesBuildItem jpaEntities, List nonJpaModels) { if (!hasEntities(jpaEntities, nonJpaModels)) { return; } resources.produce(new GeneratedResourceBuildItem("META-INF/services/io.quarkus.arc.ResourceReferenceProvider", - JPAResourceReferenceProvider.class.getName().getBytes("UTF-8"))); + JPAResourceReferenceProvider.class.getName().getBytes(StandardCharsets.UTF_8))); resourceAnnotations.produce(new ResourceAnnotationBuildItem(PERSISTENCE_CONTEXT)); resourceAnnotations.produce(new ResourceAnnotationBuildItem(PERSISTENCE_UNIT)); } @@ -335,7 +336,8 @@ public void build(HibernateOrmRecorder recorder, public void startPersistenceUnits(HibernateOrmRecorder recorder, BeanContainerBuildItem beanContainer, Optional dataSourceInitialized, JpaEntitiesBuildItem jpaEntities, List nonJpaModels, - List integrationsRuntimeConfigured) throws Exception { + List integrationsRuntimeConfigured, + Optional schemaReadyBuildItem) throws Exception { if (!hasEntities(jpaEntities, nonJpaModels)) { return; } @@ -405,6 +407,15 @@ private void handleHibernateORMWithNoPersistenceXml( systemProperty.produce(new SystemPropertyBuildItem(AvailableSettings.STORAGE_ENGINE, hibernateConfig.dialectStorageEngine.get())); } + // Physical Naming Strategy + hibernateConfig.physicalNamingStrategy.ifPresent( + namingStrategy -> desc.getProperties() + .setProperty(AvailableSettings.PHYSICAL_NAMING_STRATEGY, namingStrategy)); + + // Implicit Naming Strategy + hibernateConfig.implicitNamingStrategy.ifPresent( + namingStrategy -> desc.getProperties() + .setProperty(AvailableSettings.IMPLICIT_NAMING_STRATEGY, namingStrategy)); // Database desc.getProperties().setProperty(AvailableSettings.HBM2DDL_DATABASE_ACTION, @@ -423,6 +434,10 @@ private void handleHibernateORMWithNoPersistenceXml( hibernateConfig.database.defaultSchema.ifPresent( schema -> desc.getProperties().setProperty(AvailableSettings.DEFAULT_SCHEMA, schema)); + if (hibernateConfig.database.globallyQuotedIdentifiers) { + desc.getProperties().setProperty(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, "true"); + } + // Query if (hibernateConfig.batchFetchSize > 0) { desc.getProperties().setProperty(AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmReflections.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmReflections.java index 2f59b2871a17c..495737f60a96e 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmReflections.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmReflections.java @@ -55,7 +55,9 @@ public void registerCoreReflections(BuildProducer refl //Various well known needs: simpleConstructor(reflectiveClass, org.hibernate.tuple.entity.PojoEntityTuplizer.class); + simpleConstructor(reflectiveClass, org.hibernate.tuple.entity.DynamicMapEntityTuplizer.class); allConstructors(reflectiveClass, org.hibernate.tuple.component.PojoComponentTuplizer.class); + simpleConstructor(reflectiveClass, org.hibernate.tuple.component.DynamicMapComponentTuplizer.class); allConstructors(reflectiveClass, org.hibernate.persister.collection.OneToManyPersister.class); allConstructors(reflectiveClass, org.hibernate.persister.collection.BasicCollectionPersister.class); simpleConstructor(reflectiveClass, org.hibernate.persister.entity.SingleTableEntityPersister.class); diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/CustomImplicitNamingStrategy.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/CustomImplicitNamingStrategy.java new file mode 100644 index 0000000000000..7687491a12678 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/CustomImplicitNamingStrategy.java @@ -0,0 +1,13 @@ +package io.quarkus.hibernate.orm.naming; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ImplicitEntityNameSource; +import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl; + +public class CustomImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl { + @Override + public Identifier determinePrimaryTableName(ImplicitEntityNameSource source) { + return toIdentifier("TBL_" + source.getEntityNaming().getEntityName().replace('.', '_').toUpperCase(), + source.getBuildingContext()); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/ImplicitNamingStrategyTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/ImplicitNamingStrategyTest.java new file mode 100644 index 0000000000000..7ac2ebfc2d317 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/ImplicitNamingStrategyTest.java @@ -0,0 +1,49 @@ +package io.quarkus.hibernate.orm.naming; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; +import javax.persistence.EntityManager; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.hibernate.orm.MyEntity; +import io.quarkus.test.QuarkusUnitTest; + +public class ImplicitNamingStrategyTest { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyEntity.class, CustomImplicitNamingStrategy.class) + .addAsResource(EmptyAsset.INSTANCE, "import.sql") + .addAsResource("application-implicit-naming-strategy.properties", "application.properties")); + + @Inject + EntityManager entityManager; + + @BeforeEach + public void activateRequestContext() { + Arc.container().requestContext().activate(); + } + + @Test + public void testImplicitNamingStrategy() throws Exception { + // Check if "TBL_MyEntity" was created + Number result = (Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM TBL_IO_QUARKUS_HIBERNATE_ORM_MYENTITY") + .getSingleResult(); + assertEquals(0, result.intValue()); + } + + @AfterEach + public void terminateRequestContext() { + Arc.container().requestContext().terminate(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/PhysicalNamingStrategyTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/PhysicalNamingStrategyTest.java new file mode 100644 index 0000000000000..e92703d9f8bc1 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/PhysicalNamingStrategyTest.java @@ -0,0 +1,48 @@ +package io.quarkus.hibernate.orm.naming; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; +import javax.persistence.EntityManager; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.hibernate.orm.MyEntity; +import io.quarkus.test.QuarkusUnitTest; + +public class PhysicalNamingStrategyTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyEntity.class, PrefixPhysicalNamingStrategy.class) + .addAsResource(EmptyAsset.INSTANCE, "import.sql") + .addAsResource("application-physical-naming-strategy.properties", "application.properties")); + + @Inject + EntityManager entityManager; + + @BeforeEach + public void activateRequestContext() { + Arc.container().requestContext().activate(); + } + + @Test + public void testPhysicalNamingStrategy() throws Exception { + // Check if "TBL_MyEntity" was created + Number result = (Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM TBL_MYENTITY").getSingleResult(); + assertEquals(0, result.intValue()); + } + + @AfterEach + public void terminateRequestContext() { + Arc.container().requestContext().terminate(); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/PrefixPhysicalNamingStrategy.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/PrefixPhysicalNamingStrategy.java new file mode 100644 index 0000000000000..df2d04d0cf645 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/naming/PrefixPhysicalNamingStrategy.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.orm.naming; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; + +public class PrefixPhysicalNamingStrategy implements PhysicalNamingStrategy { + + @Override + public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) { + return name == null ? null : Identifier.toIdentifier("CTL_" + name.getText()); + } + + @Override + public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) { + return name == null ? null : Identifier.toIdentifier("SCH_" + name.getText()); + } + + @Override + public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) { + return Identifier.toIdentifier("TBL_" + name.getText()); + } + + @Override + public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) { + return Identifier.toIdentifier("SEQ_" + name.getText()); + } + + @Override + public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) { + return Identifier.toIdentifier("COL_" + name.getText()); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/Group.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/Group.java new file mode 100644 index 0000000000000..e2e6963e138cb --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/Group.java @@ -0,0 +1,39 @@ +package io.quarkus.hibernate.orm.quoted_indentifiers; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * Table with reserved name. + *

+ * http://www.h2database.com/html/advanced.html section "Keywords / Reserved Words". + */ +@Entity +@Table(name = "group") +public class Group { + + private Long id; + + private String name; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "groupSeq") + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/JPAQuotedTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/JPAQuotedTestCase.java new file mode 100644 index 0000000000000..ce49f099a34f2 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/JPAQuotedTestCase.java @@ -0,0 +1,32 @@ +package io.quarkus.hibernate.orm.quoted_indentifiers; + +import static org.hamcrest.core.StringContains.containsString; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +/** + * Failed to fetch entity with reserved name. + */ +public class JPAQuotedTestCase { + + @RegisterExtension + final static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Group.class, QuotedResource.class) + .addAsResource("application-quoted-identifiers.properties", "application.properties")); + + @Test + public void testQuotedIdentifiers() { + RestAssured.when().post("/jpa-test-quoted").then() + .body(containsString("ok")); + + RestAssured.when().get("/jpa-test-quoted").then() + .body(containsString("group_name")); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/QuotedResource.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/QuotedResource.java new file mode 100644 index 0000000000000..cd18eb45fae48 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/quoted_indentifiers/QuotedResource.java @@ -0,0 +1,44 @@ +package io.quarkus.hibernate.orm.quoted_indentifiers; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.transaction.Transactional; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * Try to fetch entity with reserved name. + */ +@Path("/jpa-test-quoted") +@ApplicationScoped +public class QuotedResource { + + @Inject + EntityManager em; + + @POST + @Produces(MediaType.TEXT_PLAIN) + @Transactional + public String create() { + Group group = new Group(); + group.setId(1L); + group.setName("group_name"); + em.merge(group); + return "ok"; + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Transactional + public String selectWithQuotedEntity() { + try { + return em.find(Group.class, 1L).getName(); + } catch (Exception e) { + return "Unable to fetch group."; + } + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-implicit-naming-strategy.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-implicit-naming-strategy.properties new file mode 100644 index 0000000000000..5af94f5d44bcd --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-implicit-naming-strategy.properties @@ -0,0 +1,7 @@ +quarkus.datasource.url=jdbc:h2:mem:test +quarkus.datasource.driver=org.h2.Driver + +quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.implicit-naming-strategy=io.quarkus.hibernate.orm.naming.CustomImplicitNamingStrategy \ No newline at end of file diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-physical-naming-strategy.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-physical-naming-strategy.properties new file mode 100644 index 0000000000000..c34f94cfc51f0 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-physical-naming-strategy.properties @@ -0,0 +1,7 @@ +quarkus.datasource.url=jdbc:h2:mem:test +quarkus.datasource.driver=org.h2.Driver + +quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.physical-naming-strategy=io.quarkus.hibernate.orm.naming.PrefixPhysicalNamingStrategy \ No newline at end of file diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-quoted-identifiers.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-quoted-identifiers.properties new file mode 100644 index 0000000000000..f8ddcea5f4c8e --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-quoted-identifiers.properties @@ -0,0 +1,8 @@ +quarkus.datasource.url=jdbc:h2:mem:test +quarkus.datasource.driver=org.h2.Driver + +quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.database.generation=drop-and-create + +quarkus.hibernate-orm.database.globally-quoted-identifiers=true diff --git a/extensions/hibernate-orm/runtime/pom.xml b/extensions/hibernate-orm/runtime/pom.xml index 491c0406cd599..2b704d7ba25ec 100644 --- a/extensions/hibernate-orm/runtime/pom.xml +++ b/extensions/hibernate-orm/runtime/pom.xml @@ -50,7 +50,7 @@ javassist - + javax.xml.bind jaxb-api @@ -67,6 +67,21 @@ + + + org.glassfish.jaxb + jaxb-runtime + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + + org.jboss.spec.javax.xml.bind + jboss-jaxb-api_2.3_spec + jakarta.persistence jakarta.persistence-api diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java index aaeb3a5421fa7..fe207ae4506c1 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java @@ -72,7 +72,8 @@ public RecordableBootstrap(BootstrapServiceRegistry bootstrapServiceRegistry) { } public RecordableBootstrap(BootstrapServiceRegistry bootstrapServiceRegistry, LoadedConfig loadedConfigBaseline) { - this.settings = QuarkusEnvironment.getInitialProperties(); + this.settings = new HashMap(); + this.settings.putAll(QuarkusEnvironment.getInitialProperties()); this.bootstrapServiceRegistry = bootstrapServiceRegistry; this.configLoader = new ConfigLoader(bootstrapServiceRegistry); this.aggregatedCfgXml = loadedConfigBaseline; diff --git a/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchClasses.java b/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchClasses.java index 4d152d6cba025..dac09fea84658 100644 --- a/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchClasses.java +++ b/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchClasses.java @@ -25,42 +25,17 @@ import org.hibernate.search.backend.elasticsearch.document.model.esnative.impl.RoutingType; import org.hibernate.search.backend.elasticsearch.index.settings.esnative.impl.Analysis; import org.hibernate.search.backend.elasticsearch.index.settings.esnative.impl.IndexSettings; -import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.declaration.MarkerBinding; -import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.declaration.PropertyBinding; -import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.declaration.RoutingKeyBinding; -import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.declaration.TypeBinding; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.AssociationInverseSide; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.DocumentId; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ScaledNumberField; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.processing.TypeMapping; import org.jboss.jandex.DotName; class HibernateSearchClasses { static final DotName INDEXED = DotName.createSimple(Indexed.class.getName()); - static final List FIELD_ANNOTATIONS = Arrays.asList( - DotName.createSimple(DocumentId.class.getName()), - DotName.createSimple(GenericField.class.getName()), - DotName.createSimple(FullTextField.class.getName()), - DotName.createSimple(KeywordField.class.getName()), - DotName.createSimple(ScaledNumberField.class.getName()), - DotName.createSimple(IndexedEmbedded.class.getName()), - DotName.createSimple(AssociationInverseSide.class.getName()), - DotName.createSimple(IndexingDependency.class.getName())); - - static final List BINDING_DECLARATION_ANNOTATIONS_ON_PROPERTIES = Arrays.asList( - DotName.createSimple(PropertyBinding.class.getName()), - DotName.createSimple(MarkerBinding.class.getName())); - - static final List BINDING_DECLARATION_ANNOTATIONS_ON_TYPES = Arrays.asList( - DotName.createSimple(TypeBinding.class.getName()), - DotName.createSimple(RoutingKeyBinding.class.getName())); + static final DotName PROPERTY_MAPPING_META_ANNOTATION = DotName.createSimple( + org.hibernate.search.mapper.pojo.mapping.definition.annotation.processing.PropertyMapping.class.getName()); + static final DotName TYPE_MAPPING_META_ANNOTATION = DotName.createSimple(TypeMapping.class.getName()); static final List SCHEMA_MAPPING_CLASSES = Arrays.asList( DotName.createSimple(AbstractTypeMapping.class.getName()), diff --git a/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchElasticsearchProcessor.java b/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchElasticsearchProcessor.java index 3a47a962aae21..fc826d3d807ce 100644 --- a/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchElasticsearchProcessor.java +++ b/extensions/hibernate-search-elasticsearch/deployment/src/main/java/io/quarkus/hibernate/search/elasticsearch/HibernateSearchElasticsearchProcessor.java @@ -1,10 +1,9 @@ package io.quarkus.hibernate.search.elasticsearch; -import static io.quarkus.hibernate.search.elasticsearch.HibernateSearchClasses.BINDING_DECLARATION_ANNOTATIONS_ON_PROPERTIES; -import static io.quarkus.hibernate.search.elasticsearch.HibernateSearchClasses.BINDING_DECLARATION_ANNOTATIONS_ON_TYPES; -import static io.quarkus.hibernate.search.elasticsearch.HibernateSearchClasses.FIELD_ANNOTATIONS; import static io.quarkus.hibernate.search.elasticsearch.HibernateSearchClasses.INDEXED; +import static io.quarkus.hibernate.search.elasticsearch.HibernateSearchClasses.PROPERTY_MAPPING_META_ANNOTATION; import static io.quarkus.hibernate.search.elasticsearch.HibernateSearchClasses.SCHEMA_MAPPING_CLASSES; +import static io.quarkus.hibernate.search.elasticsearch.HibernateSearchClasses.TYPE_MAPPING_META_ANNOTATION; import java.util.ArrayList; import java.util.HashSet; @@ -39,6 +38,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.deployment.configuration.ConfigurationError; +import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationBuildItem; import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem; import io.quarkus.hibernate.search.elasticsearch.runtime.HibernateSearchElasticsearchBuildTimeConfig; @@ -52,6 +52,11 @@ class HibernateSearchElasticsearchProcessor { HibernateSearchElasticsearchBuildTimeConfig buildTimeConfig; + @BuildStep + void setupLogFilters(BuildProducer filters) { + filters.produce(new LogCleanupFilterBuildItem("org.hibernate.search.engine.Version", "HSEARCH000034")); + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) public void build(HibernateSearchElasticsearchRecorder recorder, @@ -130,7 +135,10 @@ private static void checkConfig(HibernateSearchElasticsearchBuildTimeConfig buil private void registerReflection(IndexView index, BuildProducer reflectiveClass, BuildProducer reflectiveHierarchy) { Set reflectiveClassCollector = new HashSet<>(); - Set reflectiveTypeCollector = new HashSet<>(); + + // TODO remove this when upgrading to Beta4 (when https://hibernate.atlassian.net/browse/HSEARCH-3795 is solved) + reflectiveClass.produce( + new ReflectiveClassBuildItem(false, false, org.hibernate.search.engine.logging.impl.Log_$logger.class)); if (buildTimeConfig.defaultBackend.analysis.configurer.isPresent()) { reflectiveClass.produce( @@ -142,53 +150,38 @@ private void registerReflection(IndexView index, BuildProducer reflectiveHierarchyCollector = new HashSet<>(); + + for (AnnotationInstance propertyMappingMetaAnnotationInstance : index + .getAnnotations(PROPERTY_MAPPING_META_ANNOTATION)) { + for (AnnotationInstance propertyMappingAnnotationInstance : index + .getAnnotations(propertyMappingMetaAnnotationInstance.name())) { + AnnotationTarget annotationTarget = propertyMappingAnnotationInstance.target(); if (annotationTarget.kind() == Kind.FIELD) { FieldInfo fieldInfo = annotationTarget.asField(); - addReflectiveClass(index, reflectiveClassCollector, reflectiveTypeCollector, fieldInfo.declaringClass()); - addReflectiveType(index, reflectiveTypeCollector, fieldInfo.type()); + addReflectiveClass(index, reflectiveClassCollector, reflectiveHierarchyCollector, + fieldInfo.declaringClass()); + addReflectiveType(index, reflectiveClassCollector, reflectiveHierarchyCollector, + fieldInfo.type()); } else if (annotationTarget.kind() == Kind.METHOD) { MethodInfo methodInfo = annotationTarget.asMethod(); - addReflectiveClass(index, reflectiveClassCollector, reflectiveTypeCollector, methodInfo.declaringClass()); - addReflectiveType(index, reflectiveTypeCollector, methodInfo.returnType()); + addReflectiveClass(index, reflectiveClassCollector, reflectiveHierarchyCollector, + methodInfo.declaringClass()); + addReflectiveType(index, reflectiveClassCollector, reflectiveHierarchyCollector, + methodInfo.returnType()); } } } - Set reflectiveHierarchyCollector = new HashSet<>(); - - for (DotName bindingDeclarationOnProperties : BINDING_DECLARATION_ANNOTATIONS_ON_PROPERTIES) { - for (AnnotationInstance propertyBridgeMappingInstance : index.getAnnotations(bindingDeclarationOnProperties)) { - for (AnnotationInstance propertyBridgeInstance : index.getAnnotations(propertyBridgeMappingInstance.name())) { - AnnotationTarget annotationTarget = propertyBridgeInstance.target(); - if (annotationTarget.kind() == Kind.FIELD) { - FieldInfo fieldInfo = annotationTarget.asField(); - addReflectiveClass(index, reflectiveClassCollector, reflectiveTypeCollector, - fieldInfo.declaringClass()); - reflectiveHierarchyCollector.add(fieldInfo.type()); - } else if (annotationTarget.kind() == Kind.METHOD) { - MethodInfo methodInfo = annotationTarget.asMethod(); - addReflectiveClass(index, reflectiveClassCollector, reflectiveTypeCollector, - methodInfo.declaringClass()); - reflectiveHierarchyCollector.add(methodInfo.returnType()); - } - } - } - } - - for (DotName bindingDeclarationOnTypes : BINDING_DECLARATION_ANNOTATIONS_ON_TYPES) { - for (AnnotationInstance typeBridgeMappingInstance : index.getAnnotations(bindingDeclarationOnTypes)) { - for (AnnotationInstance typeBridgeInstance : index.getAnnotations(typeBridgeMappingInstance.name())) { - addReflectiveClass(index, reflectiveClassCollector, reflectiveTypeCollector, - typeBridgeInstance.target().asClass()); - } + for (AnnotationInstance typeBridgeMappingInstance : index.getAnnotations(TYPE_MAPPING_META_ANNOTATION)) { + for (AnnotationInstance typeBridgeInstance : index.getAnnotations(typeBridgeMappingInstance.name())) { + addReflectiveClass(index, reflectiveClassCollector, reflectiveHierarchyCollector, + typeBridgeInstance.target().asClass()); } } String[] reflectiveClasses = Stream - .of(reflectiveClassCollector.stream(), reflectiveTypeCollector.stream(), SCHEMA_MAPPING_CLASSES.stream()) + .of(reflectiveClassCollector.stream(), SCHEMA_MAPPING_CLASSES.stream()) .flatMap(Function.identity()).map(c -> c.toString()).toArray(String[]::new); reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, reflectiveClasses)); @@ -198,7 +191,7 @@ private void registerReflection(IndexView index, BuildProducer reflectiveClassCollector, - Set reflectiveTypeCollector, ClassInfo classInfo) { + Set reflectiveTypeCollector, ClassInfo classInfo) { if (skipClass(classInfo.name(), reflectiveClassCollector)) { return; } @@ -219,29 +212,27 @@ private static void addReflectiveClass(IndexView index, Set reflectiveC } else if (superClassType instanceof ParameterizedType) { ParameterizedType parameterizedType = superClassType.asParameterizedType(); for (Type typeArgument : parameterizedType.arguments()) { - addReflectiveType(index, reflectiveTypeCollector, typeArgument); + addReflectiveType(index, reflectiveClassCollector, reflectiveTypeCollector, typeArgument); } superClassType = parameterizedType.owner(); } } } - private static void addReflectiveType(IndexView index, Set reflectiveTypeCollector, Type type) { + private static void addReflectiveType(IndexView index, Set reflectiveClassCollector, + Set reflectiveTypeCollector, Type type) { if (type instanceof VoidType || type instanceof PrimitiveType || type instanceof UnresolvedTypeVariable) { return; } else if (type instanceof ClassType) { - if (skipClass(type.name(), reflectiveTypeCollector)) { - return; - } - - reflectiveTypeCollector.add(type.name()); + ClassInfo classInfo = index.getClassByName(type.name()); + addReflectiveClass(index, reflectiveClassCollector, reflectiveTypeCollector, classInfo); } else if (type instanceof ArrayType) { - addReflectiveType(index, reflectiveTypeCollector, type.asArrayType().component()); + addReflectiveType(index, reflectiveClassCollector, reflectiveTypeCollector, type.asArrayType().component()); } else if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = type.asParameterizedType(); - addReflectiveType(index, reflectiveTypeCollector, parameterizedType.owner()); + addReflectiveType(index, reflectiveClassCollector, reflectiveTypeCollector, parameterizedType.owner()); for (Type typeArgument : parameterizedType.arguments()) { - addReflectiveType(index, reflectiveTypeCollector, typeArgument); + addReflectiveType(index, reflectiveClassCollector, reflectiveTypeCollector, typeArgument); } } } diff --git a/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java b/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java index fdd8b13c7cc3b..a50b41de6d1b2 100644 --- a/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java +++ b/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java @@ -8,7 +8,6 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.function.BiConsumer; -import java.util.function.Function; import org.hibernate.boot.Metadata; import org.hibernate.boot.spi.BootstrapContext; @@ -124,8 +123,9 @@ private void contributeBackendBuildTimeProperties(BiConsumer pro private void contributeBackendRuntimeProperties(BiConsumer propertyCollector, String backendName, ElasticsearchBackendRuntimeConfig elasticsearchBackendConfig) { addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.HOSTS, - elasticsearchBackendConfig.hosts, - v -> (!v.isEmpty() && !(v.size() == 1 && v.get(0).isEmpty())), Function.identity()); + elasticsearchBackendConfig.hosts); + addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.PROTOCOL, + elasticsearchBackendConfig.protocol.getHibernateSearchString()); addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.USERNAME, elasticsearchBackendConfig.username); addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.PASSWORD, @@ -142,8 +142,6 @@ private void contributeBackendRuntimeProperties(BiConsumer prope if (elasticsearchBackendConfig.discovery.enabled) { addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.DISCOVERY_REFRESH_INTERVAL, elasticsearchBackendConfig.discovery.refreshInterval.getSeconds()); - addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.DISCOVERY_SCHEME, - elasticsearchBackendConfig.discovery.defaultScheme); } addBackendDefaultIndexConfig(propertyCollector, backendName, ElasticsearchIndexSettings.LIFECYCLE_STRATEGY, diff --git a/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfig.java b/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfig.java index 04aee3fccd968..89e067def4547 100644 --- a/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfig.java +++ b/extensions/hibernate-search-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfig.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -9,6 +10,8 @@ import org.hibernate.search.backend.elasticsearch.index.IndexStatus; import org.hibernate.search.mapper.orm.automaticindexing.AutomaticIndexingSynchronizationStrategyName; import org.hibernate.search.mapper.orm.search.loading.EntityLoadingCacheLookupStrategy; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.common.impl.StringHelper; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigDocSection; @@ -62,9 +65,16 @@ public static class ElasticsearchBackendRuntimeConfig { /** * The list of hosts of the Elasticsearch servers. */ - @ConfigItem(defaultValue = "http://localhost:9200") + @ConfigItem(defaultValue = "localhost:9200") List hosts; + /** + * The protocol to use when contacting Elasticsearch servers. + * Set to "https" to enable SSL/TLS. + */ + @ConfigItem(defaultValue = "http") + ElasticsearchClientProtocol protocol; + /** * The username used for authentication. */ @@ -115,6 +125,40 @@ public static class ElasticsearchBackendRuntimeConfig { Map indexes; } + public enum ElasticsearchClientProtocol { + /** + * Use clear-text HTTP, with SSL/TLS disabled. + */ + HTTP("http"), + /** + * Use HTTPS, with SSL/TLS enabled. + */ + HTTPS("https"); + + public static ElasticsearchClientProtocol of(String value) { + return StringHelper.parseDiscreteValues( + values(), + ElasticsearchClientProtocol::getHibernateSearchString, + (invalidValue, validValues) -> new SearchException( + String.format( + Locale.ROOT, + "Invalid protocol: '%1$s'. Valid protocols are: %2$s.", + invalidValue, + validValues)), + value); + } + + private final String hibernateSearchString; + + ElasticsearchClientProtocol(String hibernateSearchString) { + this.hibernateSearchString = hibernateSearchString; + } + + public String getHibernateSearchString() { + return hibernateSearchString; + } + } + @ConfigGroup public static class ElasticsearchIndexConfig { /** @@ -139,11 +183,6 @@ public static class DiscoveryConfig { @ConfigItem(defaultValue = "10S") Duration refreshInterval; - /** - * The scheme that should be used for the new nodes discovered. - */ - @ConfigItem(defaultValue = "http") - String defaultScheme; } @ConfigGroup diff --git a/extensions/hibernate-validator/deployment/pom.xml b/extensions/hibernate-validator/deployment/pom.xml index 31861e0f2485a..8ce58a436e719 100644 --- a/extensions/hibernate-validator/deployment/pom.xml +++ b/extensions/hibernate-validator/deployment/pom.xml @@ -41,6 +41,16 @@ assertj-core test + + io.quarkus + quarkus-resteasy-jsonb-deployment + test + + + io.rest-assured + rest-assured + test + diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/ClassLevelConstraint.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/ClassLevelConstraint.java new file mode 100644 index 0000000000000..d2a626c8c7e79 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/ClassLevelConstraint.java @@ -0,0 +1,23 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = { ClassLevelValidator.class }) +@Documented +public @interface ClassLevelConstraint { + + String message() default "My class constraint message"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/ClassLevelValidator.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/ClassLevelValidator.java new file mode 100644 index 0000000000000..60bee5bd083ee --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/ClassLevelValidator.java @@ -0,0 +1,11 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class ClassLevelValidator implements ConstraintValidator { + @Override + public boolean isValid(TestBean bean, ConstraintValidatorContext context) { + return false; + } +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DependentTestBean.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DependentTestBean.java new file mode 100644 index 0000000000000..2557dcada50f0 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DependentTestBean.java @@ -0,0 +1,10 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import javax.enterprise.context.Dependent; + +@Dependent +public class DependentTestBean { + public String testMethod(/* */ String message) { + return message; + } +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DevModeConstraintValidationTest.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DevModeConstraintValidationTest.java new file mode 100644 index 0000000000000..7c43a10fbc555 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DevModeConstraintValidationTest.java @@ -0,0 +1,130 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import static org.hamcrest.Matchers.containsString; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class DevModeConstraintValidationTest { + + @RegisterExtension + static final QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(TestBean.class, + DevModeTestResource.class, ClassLevelConstraint.class, ClassLevelValidator.class, DependentTestBean.class)); + + @Test + public void testClassConstraintHotReplacement() { + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("ok")); + + TEST.modifySourceFile("TestBean.java", + s -> s.replace("// ", "@io.quarkus.hibernate.validator.test.devmode.ClassLevelConstraint")); + + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("My class constraint message")); + } + + @Test + public void testPropertyConstraintHotReplacement() { + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("ok")); + + TEST.modifySourceFile("TestBean.java", s -> s.replace("// ", + "@javax.validation.constraints.NotNull(message=\"My property message\")")); + + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("My property message")); + } + + @Test + public void testMethodConstraintHotReplacement() { + + RestAssured.given() + .when() + .get("/test/mymessage") + .then() + .body(containsString("mymessage")); + + TEST.modifySourceFile("DependentTestBean.java", s -> s.replace("/* */", + "@javax.validation.constraints.Size(max=1, message=\"My method message\")")); + + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .get("/test/mymessage") + .then() + .body(containsString("My method message")); + } + + @Test + public void testNewBeanHotReplacement() { + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("ok")); + + TEST.addSourceFile(NewTestBean.class); + TEST.modifySourceFile("DevModeTestResource.java", s -> s.replace("@Valid TestBean", + "@Valid NewTestBean")); + + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("My new bean message")); + } + + @Test + public void testNewConstraintHotReplacement() { + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("ok")); + + TEST.addSourceFile(NewConstraint.class); + TEST.addSourceFile(NewValidator.class); + TEST.modifySourceFile("TestBean.java", s -> s.replace("// ", + "@NewConstraint")); + + RestAssured.given() + .header("Content-Type", "application/json") + .when() + .body("{}") + .post("/test/validate") + .then() + .body(containsString("My new constraint message")); + } +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DevModeTestResource.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DevModeTestResource.java new file mode 100644 index 0000000000000..3b2377ee66239 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/DevModeTestResource.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import javax.inject.Inject; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/test") +public class DevModeTestResource { + + @Inject + DependentTestBean bean; + + @POST + @Path("/validate") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + public String validateBean(@Valid TestBean testBean) { + return "ok"; + } + + @GET + @Path("/{message}") + @Produces(MediaType.TEXT_PLAIN) + public String validateCDIBean(@PathParam("message") String message) { + return bean.testMethod(message); + } +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewConstraint.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewConstraint.java new file mode 100644 index 0000000000000..1f5b8c815ecc0 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewConstraint.java @@ -0,0 +1,23 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = { NewValidator.class }) +@Documented +public @interface NewConstraint { + + String message() default "My new constraint message"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewTestBean.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewTestBean.java new file mode 100644 index 0000000000000..fed21ad884cd4 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewTestBean.java @@ -0,0 +1,8 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import javax.validation.constraints.NotNull; + +public class NewTestBean { + @NotNull(message = "My new bean message") + public String name; +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewValidator.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewValidator.java new file mode 100644 index 0000000000000..118ab4ce0cd40 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/NewValidator.java @@ -0,0 +1,11 @@ +package io.quarkus.hibernate.validator.test.devmode; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class NewValidator implements ConstraintValidator { + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return false; + } +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/TestBean.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/TestBean.java new file mode 100644 index 0000000000000..ddd1ec5b1ce88 --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/devmode/TestBean.java @@ -0,0 +1,16 @@ +package io.quarkus.hibernate.validator.test.devmode; + +// +public class TestBean { + + public String name; + + // + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/infinispan-client/deployment/pom.xml b/extensions/infinispan-client/deployment/pom.xml index 40c9a1d095e9d..053d981f0db70 100644 --- a/extensions/infinispan-client/deployment/pom.xml +++ b/extensions/infinispan-client/deployment/pom.xml @@ -21,6 +21,10 @@ io.quarkus quarkus-arc-deployment + + io.quarkus + quarkus-elytron-security-common-deployment + io.quarkus quarkus-infinispan-client @@ -33,6 +37,10 @@ io.quarkus quarkus-netty-deployment + + io.quarkus + quarkus-jsonp-deployment + diff --git a/extensions/infinispan-client/runtime/pom.xml b/extensions/infinispan-client/runtime/pom.xml index a1f9138c2e6b9..0cc55e17b2603 100644 --- a/extensions/infinispan-client/runtime/pom.xml +++ b/extensions/infinispan-client/runtime/pom.xml @@ -29,6 +29,14 @@ io.quarkus quarkus-netty + + io.quarkus + quarkus-jsonp + + + io.quarkus + quarkus-elytron-security-common + org.wildfly.security wildfly-elytron-sasl-plain diff --git a/extensions/infinispan-embedded/runtime/pom.xml b/extensions/infinispan-embedded/runtime/pom.xml index 3b10ece87523f..ea450674da85d 100644 --- a/extensions/infinispan-embedded/runtime/pom.xml +++ b/extensions/infinispan-embedded/runtime/pom.xml @@ -52,6 +52,10 @@ narayana-jta true + + jakarta.transaction + jakarta.transaction-api + com.oracle.substratevm svm diff --git a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java index ec7837e443642..71c2239c4bce1 100755 --- a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java +++ b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java @@ -4,6 +4,7 @@ import static org.jboss.jandex.AnnotationTarget.Kind.FIELD; import static org.jboss.jandex.AnnotationTarget.Kind.METHOD; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -42,6 +43,7 @@ import io.quarkus.gizmo.ResultHandle; import io.quarkus.jackson.ObjectMapperCustomizer; import io.quarkus.jackson.ObjectMapperProducer; +import io.quarkus.jackson.spi.ClassPathJacksonModuleBuildItem; import io.quarkus.jackson.spi.JacksonModuleBuildItem; public class JacksonProcessor { @@ -50,6 +52,14 @@ public class JacksonProcessor { private static final DotName JSON_SERIALIZE = DotName.createSimple(JsonSerialize.class.getName()); private static final DotName BUILDER_VOID = DotName.createSimple(Void.class.getName()); + private static final String TIME_MODULE = "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"; + private static final String JDK8_MODULE = "com.fasterxml.jackson.datatype.jdk8.Jdk8Module"; + private static final String PARAMETER_NAMES_MODULE = "com.fasterxml.jackson.module.paramnames.ParameterNamesModule"; + + // this list can probably be enriched with more modules + private static final List MODULES_NAMES_TO_AUTO_REGISTER = Arrays.asList(TIME_MODULE, JDK8_MODULE, + PARAMETER_NAMES_MODULE); + @Inject BuildProducer reflectiveClass; @@ -130,12 +140,28 @@ private void addReflectiveClass(boolean methods, boolean fields, String... class reflectiveClass.produce(new ReflectiveClassBuildItem(methods, fields, className)); } - // Generate a ObjectMapperCustomizer bean that registers each serializer / deserializer with ObjectMapper + @BuildStep + void autoRegisterModules(BuildProducer classPathJacksonModules) { + for (String module : MODULES_NAMES_TO_AUTO_REGISTER) { + registerModuleIfOnClassPath(module, classPathJacksonModules); + } + } + + private void registerModuleIfOnClassPath(String moduleClassName, + BuildProducer classPathJacksonModules) { + try { + Class.forName(moduleClassName, false, Thread.currentThread().getContextClassLoader()); + classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(moduleClassName)); + } catch (Exception ignored) { + } + } + + // Generate a ObjectMapperCustomizer bean that registers each serializer / deserializer as well as detected modules with the ObjectMapper @BuildStep void generateCustomizer(BuildProducer generatedBeans, - List jacksonModules) { + List jacksonModules, List classPathJacksonModules) { - if (jacksonModules.isEmpty()) { + if (jacksonModules.isEmpty() && classPathJacksonModules.isEmpty()) { return; } @@ -196,6 +222,14 @@ void generateCustomizer(BuildProducer generatedBeans, objectMapper, module); } + for (ClassPathJacksonModuleBuildItem classPathJacksonModule : classPathJacksonModules) { + ResultHandle module = customize + .newInstance(MethodDescriptor.ofConstructor(classPathJacksonModule.getModuleClassName())); + customize.invokeVirtualMethod( + MethodDescriptor.ofMethod(ObjectMapper.class, "registerModule", ObjectMapper.class, Module.class), + objectMapper, module); + } + customize.returnValue(null); } } diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java index b5be5c822b68f..ddc397f7e0ba3 100644 --- a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java @@ -6,9 +6,6 @@ import javax.inject.Singleton; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import io.quarkus.arc.DefaultBean; @@ -20,7 +17,6 @@ public class ObjectMapperProducer { @Produces public ObjectMapper objectMapper(Instance customizers) { ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.registerModules(new Jdk8Module(), new JavaTimeModule(), new ParameterNamesModule()); for (ObjectMapperCustomizer customizer : customizers) { customizer.customize(objectMapper); } diff --git a/extensions/jackson/spi/src/main/java/io/quarkus/jackson/spi/ClassPathJacksonModuleBuildItem.java b/extensions/jackson/spi/src/main/java/io/quarkus/jackson/spi/ClassPathJacksonModuleBuildItem.java new file mode 100644 index 0000000000000..bedf70a98a80b --- /dev/null +++ b/extensions/jackson/spi/src/main/java/io/quarkus/jackson/spi/ClassPathJacksonModuleBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.jackson.spi; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * BuildItem used to signal that some Jackson module has been detected on the classpath + * + * The modules are then registered with the ObjectMapper. + * + * Note: Modules are assumed to have a default constructor + */ +public final class ClassPathJacksonModuleBuildItem extends MultiBuildItem { + + private final String moduleClassName; + + public ClassPathJacksonModuleBuildItem(String moduleClassName) { + this.moduleClassName = moduleClassName; + } + + public String getModuleClassName() { + return moduleClassName; + } +} diff --git a/extensions/jaeger/deployment/src/main/java/io/quarkus/jaeger/deployment/JaegerProcessor.java b/extensions/jaeger/deployment/src/main/java/io/quarkus/jaeger/deployment/JaegerProcessor.java index 97cf0ba37b5bc..35e53d2176d8b 100644 --- a/extensions/jaeger/deployment/src/main/java/io/quarkus/jaeger/deployment/JaegerProcessor.java +++ b/extensions/jaeger/deployment/src/main/java/io/quarkus/jaeger/deployment/JaegerProcessor.java @@ -8,6 +8,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.jaeger.runtime.JaegerBuildTimeConfig; import io.quarkus.jaeger.runtime.JaegerConfig; import io.quarkus.jaeger.runtime.JaegerDeploymentRecorder; @@ -16,19 +17,14 @@ public class JaegerProcessor { @Inject BuildProducer extensionSslNativeSupport; - /** - * The jaeger configuration - */ - JaegerConfig jaeger; - @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - void setupTracer(JaegerDeploymentRecorder jdr) { + void setupTracer(JaegerDeploymentRecorder jdr, JaegerBuildTimeConfig buildTimeConfig, JaegerConfig jaeger) { // Indicates that this extension would like the SSL support to be enabled extensionSslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(FeatureBuildItem.JAEGER)); - if (jaeger.enabled) { + if (buildTimeConfig.enabled) { jdr.registerTracer(jaeger); } } diff --git a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerBuildTimeConfig.java b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerBuildTimeConfig.java new file mode 100644 index 0000000000000..e3e2e26e01af6 --- /dev/null +++ b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerBuildTimeConfig.java @@ -0,0 +1,17 @@ +package io.quarkus.jaeger.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * The Jaeger build time configuration. + */ +@ConfigRoot +public class JaegerBuildTimeConfig { + /** + * Defines if the Jaeger extension is enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean enabled; + +} diff --git a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerConfig.java b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerConfig.java index 14ad7472779de..8c8cc2f78b9fe 100644 --- a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerConfig.java +++ b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerConfig.java @@ -16,12 +16,6 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class JaegerConfig { - /** - * Defines if the Jaeger extension is enabled. - */ - @ConfigItem(defaultValue = "true") - public boolean enabled; - /** * The traces endpoint, in case the client should connect directly to the Collector, * like http://jaeger-collector:14268/api/traces @@ -117,4 +111,10 @@ public class JaegerConfig { @ConfigItem public Optional senderFactory; + /** + * Whether the trace context should be logged. + */ + @ConfigItem(defaultValue = "true") + public Boolean logTraceContext; + } diff --git a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerDeploymentRecorder.java b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerDeploymentRecorder.java index 6fd354b1711b3..63e8f83b77b6d 100644 --- a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerDeploymentRecorder.java +++ b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/JaegerDeploymentRecorder.java @@ -60,6 +60,8 @@ private void initTracerConfig(JaegerConfig jaeger) { initTracerProperty("JAEGER_TAGS", jaeger.tags, tags -> tags.toString()); initTracerProperty("JAEGER_PROPAGATION", jaeger.propagation, format -> format.toString()); initTracerProperty("JAEGER_SENDER_FACTORY", jaeger.senderFactory, sender -> sender); + initTracerProperty(QuarkusJaegerTracer.LOG_TRACE_CONTEXT, Optional.of(jaeger.logTraceContext), + logTraceContext -> logTraceContext.toString()); } private void initTracerProperty(String property, Optional value, Function accessor) { diff --git a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/MDCScope.java b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/MDCScope.java new file mode 100644 index 0000000000000..e2e8c65c8867f --- /dev/null +++ b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/MDCScope.java @@ -0,0 +1,48 @@ +package io.quarkus.jaeger.runtime; + +import org.jboss.logging.MDC; + +import io.jaegertracing.internal.JaegerSpanContext; +import io.opentracing.Scope; +import io.opentracing.Span; + +/** + * Scope that sets span context into MDC. + */ +public class MDCScope implements Scope { + + /** + * MDC keys + */ + private static final String TRACE_ID = "traceId"; + private static final String SPAN_ID = "spanId"; + private static final String SAMPLED = "sampled"; + + private final Scope wrapped; + + public MDCScope(Scope scope) { + this.wrapped = scope; + if (scope.span().context() instanceof JaegerSpanContext) { + putContext((JaegerSpanContext) scope.span().context()); + } + } + + @Override + public void close() { + wrapped.close(); + MDC.remove(TRACE_ID); + MDC.remove(SPAN_ID); + MDC.remove(SAMPLED); + } + + @Override + public Span span() { + return wrapped.span(); + } + + protected void putContext(JaegerSpanContext spanContext) { + MDC.put(TRACE_ID, spanContext.getTraceId()); + MDC.put(SPAN_ID, String.format("%16x", spanContext.getSpanId())); + MDC.put(SAMPLED, Boolean.toString(spanContext.isSampled())); + } +} diff --git a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/MDCScopeManager.java b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/MDCScopeManager.java new file mode 100644 index 0000000000000..3c1cedef03266 --- /dev/null +++ b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/MDCScopeManager.java @@ -0,0 +1,24 @@ +package io.quarkus.jaeger.runtime; + +import io.opentracing.Scope; +import io.opentracing.ScopeManager; +import io.opentracing.Span; + +public class MDCScopeManager implements ScopeManager { + + private final ScopeManager wrapped; + + public MDCScopeManager(ScopeManager scopeManager) { + this.wrapped = scopeManager; + } + + @Override + public Scope activate(Span span, boolean finishSpanOnClose) { + return new MDCScope(wrapped.activate(span, finishSpanOnClose)); + } + + @Override + public Scope active() { + return wrapped.active(); + } +} diff --git a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/QuarkusJaegerTracer.java b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/QuarkusJaegerTracer.java index dc5938ef173b9..37811ff44fc74 100644 --- a/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/QuarkusJaegerTracer.java +++ b/extensions/jaeger/runtime/src/main/java/io/quarkus/jaeger/runtime/QuarkusJaegerTracer.java @@ -6,9 +6,11 @@ import io.opentracing.SpanContext; import io.opentracing.Tracer; import io.opentracing.propagation.Format; +import io.opentracing.util.ThreadLocalScopeManager; public class QuarkusJaegerTracer implements Tracer { + static final String LOG_TRACE_CONTEXT = "JAEGER_LOG_TRACE_CONTEXT"; private static volatile Tracer tracer; @Override @@ -21,13 +23,25 @@ private static Tracer tracer() { synchronized (QuarkusJaegerTracer.class) { if (tracer == null) { tracer = Configuration.fromEnv() - .withMetricsFactory(new QuarkusJaegerMetricsFactory()).getTracer(); + .withMetricsFactory(new QuarkusJaegerMetricsFactory()) + .getTracerBuilder() + .withScopeManager(getScopeManager()) + .build(); } } } return tracer; } + private static ScopeManager getScopeManager() { + ScopeManager scopeManager = new ThreadLocalScopeManager(); + String logTraceContext = System.getProperty(LOG_TRACE_CONTEXT); + if ("true".equals(logTraceContext)) { + scopeManager = new MDCScopeManager(scopeManager); + } + return scopeManager; + } + @Override public SpanBuilder buildSpan(String operationName) { return tracer().buildSpan(operationName); diff --git a/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java index 722aeb05b54bb..d8f62b43e8b79 100644 --- a/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java +++ b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java @@ -218,8 +218,9 @@ private void handleJaxbFile(Path p) { addResource(path); for (String line : Files.readAllLines(p)) { - if (!line.startsWith("#")) { - String clazz = pkg + line.trim(); + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + String clazz = pkg + line; Class cl = Class.forName(clazz); while (cl != Object.class) { diff --git a/extensions/jdbc/jdbc-h2/runtime/src/main/java/io/quarkus/jdbc/h2/runtime/graal/Engine.java b/extensions/jdbc/jdbc-h2/runtime/src/main/java/io/quarkus/jdbc/h2/runtime/graal/Engine.java index f3ec0284016e5..c288359903828 100644 --- a/extensions/jdbc/jdbc-h2/runtime/src/main/java/io/quarkus/jdbc/h2/runtime/graal/Engine.java +++ b/extensions/jdbc/jdbc-h2/runtime/src/main/java/io/quarkus/jdbc/h2/runtime/graal/Engine.java @@ -3,6 +3,7 @@ import org.h2.engine.ConnectionInfo; import org.h2.engine.Session; +import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; @@ -10,6 +11,11 @@ @Substitute public final class Engine { + @Substitute + public static org.h2.engine.Engine getInstance() { + return SubstrateUtil.cast(new Engine(), org.h2.engine.Engine.class); + } + @Substitute public Session createSession(ConnectionInfo ci) { throw new UnsupportedOperationException( diff --git a/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java b/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java index 4fe0b587ffa91..2b2dd77803514 100644 --- a/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java +++ b/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java @@ -3,6 +3,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; public final class MariaDBJDBCReflections { @@ -17,4 +18,11 @@ void build(BuildProducer reflectiveClass) { //MariaDB's connection process requires reflective read to all fields of Options: reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, "org.mariadb.jdbc.internal.util.Options")); } + + @BuildStep + void runtimeInit(BuildProducer runtimeInitialized) { + //MastersSlavesListener starts threads in DynamicSizedSchedulerImpl which is disallowed during build time in GraalVM + runtimeInitialized + .produce(new RuntimeInitializedClassBuildItem("org.mariadb.jdbc.internal.failover.impl.MastersSlavesListener")); + } } diff --git a/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/DefaultAuthenticationProvider_Substitutions.java b/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/AuthenticationPluginLoader_Substitutions.java similarity index 57% rename from extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/DefaultAuthenticationProvider_Substitutions.java rename to extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/AuthenticationPluginLoader_Substitutions.java index e8f9a350bc9d3..a486d4361947a 100644 --- a/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/DefaultAuthenticationProvider_Substitutions.java +++ b/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/AuthenticationPluginLoader_Substitutions.java @@ -2,22 +2,20 @@ import java.sql.SQLException; -import org.mariadb.jdbc.internal.com.send.authentication.AuthenticationPlugin; +import org.mariadb.jdbc.authentication.AuthenticationPlugin; +import org.mariadb.jdbc.authentication.AuthenticationPluginLoader; import org.mariadb.jdbc.internal.com.send.authentication.ClearPasswordPlugin; import org.mariadb.jdbc.internal.com.send.authentication.Ed25519PasswordPlugin; import org.mariadb.jdbc.internal.com.send.authentication.NativePasswordPlugin; import org.mariadb.jdbc.internal.com.send.authentication.OldPasswordPlugin; import org.mariadb.jdbc.internal.com.send.authentication.SendGssApiAuthPacket; -// import org.mariadb.jdbc.internal.com.send.authentication.SendPamAuthPacket; -import org.mariadb.jdbc.internal.protocol.authentication.DefaultAuthenticationProvider; -import org.mariadb.jdbc.internal.util.Options; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; -@TargetClass(DefaultAuthenticationProvider.class) +@TargetClass(AuthenticationPluginLoader.class) @Substitute -public final class DefaultAuthenticationProvider_Substitutions { +public final class AuthenticationPluginLoader_Substitutions { public static final String MYSQL_NATIVE_PASSWORD = "mysql_native_password"; public static final String MYSQL_OLD_PASSWORD = "mysql_old_password"; @@ -27,30 +25,26 @@ public final class DefaultAuthenticationProvider_Substitutions { private static final String DIALOG = "dialog"; @Substitute - public static AuthenticationPlugin processAuthPlugin(String plugin, - String password, - byte[] authData, - Options options) - throws SQLException { - switch (plugin) { + public static AuthenticationPlugin get(String type) throws SQLException { + switch (type) { case MYSQL_NATIVE_PASSWORD: - return new NativePasswordPlugin(password, authData, options.passwordCharacterEncoding); + return new NativePasswordPlugin(); case MYSQL_OLD_PASSWORD: - return new OldPasswordPlugin(password, authData); + return new OldPasswordPlugin(); case MYSQL_CLEAR_PASSWORD: - return new ClearPasswordPlugin(password, options.passwordCharacterEncoding); + return new ClearPasswordPlugin(); case DIALOG: throw new UnsupportedOperationException("Authentication strategy 'dialog' is not supported in GraalVM"); - //return new SendPamAuthPacket(password, authData, options.passwordCharacterEncoding); + //return new SendPamAuthPacket(); case GSSAPI_CLIENT: - return new SendGssApiAuthPacket(authData, options.servicePrincipalName); + return new SendGssApiAuthPacket(); case MYSQL_ED25519_PASSWORD: - return new Ed25519PasswordPlugin(password, authData, options.passwordCharacterEncoding); + return new Ed25519PasswordPlugin(); default: throw new SQLException( "Client does not support authentication protocol requested by server. " - + "Consider upgrading MariaDB client. plugin was = " + plugin, + + "Consider upgrading MariaDB client. plugin was = " + type, "08004", 1251); } } diff --git a/extensions/jdbc/jdbc-mssql/deployment/src/main/java/io/quarkus/jdbc/mssql/deployment/MsSQLProcessor.java b/extensions/jdbc/jdbc-mssql/deployment/src/main/java/io/quarkus/jdbc/mssql/deployment/MsSQLProcessor.java index 9e33e50941aee..87e0d44d486ac 100644 --- a/extensions/jdbc/jdbc-mssql/deployment/src/main/java/io/quarkus/jdbc/mssql/deployment/MsSQLProcessor.java +++ b/extensions/jdbc/jdbc-mssql/deployment/src/main/java/io/quarkus/jdbc/mssql/deployment/MsSQLProcessor.java @@ -3,8 +3,9 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.NativeEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; public class MsSQLProcessor { @@ -15,9 +16,13 @@ FeatureBuildItem feature() { @BuildStep void nativeResources(BuildProducer resources, - BuildProducer nativeEnableAllCharsets) { + BuildProducer nativeEnableAllCharsets) { resources.produce(new NativeImageResourceBundleBuildItem("com.microsoft.sqlserver.jdbc.SQLServerResource")); - nativeEnableAllCharsets.produce(new NativeEnableAllCharsetsBuildItem()); + nativeEnableAllCharsets.produce(new NativeImageEnableAllCharsetsBuildItem()); } + @BuildStep + public RuntimeInitializedClassBuildItem runtimeInitializedClass() { + return new RuntimeInitializedClassBuildItem("com.microsoft.sqlserver.jdbc.KerbAuthentication"); + } } diff --git a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java index 5103a7e49e7ce..176c4d561348d 100644 --- a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java +++ b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java @@ -2,7 +2,8 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.NativeEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllTimeZonesBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; public class JDBCMySQLProcessor { @@ -17,7 +18,12 @@ NativeImageResourceBuildItem resource() { } @BuildStep - NativeEnableAllCharsetsBuildItem enableAllCharsets() { - return new NativeEnableAllCharsetsBuildItem(); + NativeImageEnableAllCharsetsBuildItem enableAllCharsets() { + return new NativeImageEnableAllCharsetsBuildItem(); + } + + @BuildStep + NativeImageEnableAllTimeZonesBuildItem enableAllTimeZones() { + return new NativeImageEnableAllTimeZonesBuildItem(); } } diff --git a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java index 009b8035d51dd..a85da95ca6307 100644 --- a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java +++ b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java @@ -40,16 +40,24 @@ public final class MySQLJDBCReflections { @BuildStep void registerDriverForReflection(BuildProducer reflectiveClass) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.jdbc.Driver.class.getName())); + reflectiveClass.produce( + new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.FailoverDnsSrvConnectionUrl.class.getName())); reflectiveClass.produce( new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.FailoverConnectionUrl.class.getName())); reflectiveClass .produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.SingleConnectionUrl.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.LoadbalanceConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.LoadBalanceConnectionUrl.class.getName())); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, + com.mysql.cj.conf.url.LoadBalanceDnsSrvConnectionUrl.class.getName())); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, + com.mysql.cj.conf.url.ReplicationDnsSrvConnectionUrl.class.getName())); reflectiveClass.produce( new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.ReplicationConnectionUrl.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.XDevAPIConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.XDevApiConnectionUrl.class.getName())); + reflectiveClass.produce( + new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.XDevApiDnsSrvConnectionUrl.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.jdbc.ha.LoadBalancedAutoCommitInterceptor.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.log.StandardLogger.class.getName())); diff --git a/extensions/jgit/deployment/pom.xml b/extensions/jgit/deployment/pom.xml index 718cadd3e92ab..37c9edb7af380 100644 --- a/extensions/jgit/deployment/pom.xml +++ b/extensions/jgit/deployment/pom.xml @@ -18,6 +18,10 @@ io.quarkus quarkus-core-deployment + + io.quarkus + quarkus-jsch-deployment + io.quarkus quarkus-jgit diff --git a/extensions/jgit/deployment/src/main/java/io/quarkus/jgit/deployment/JGitProcessor.java b/extensions/jgit/deployment/src/main/java/io/quarkus/jgit/deployment/JGitProcessor.java new file mode 100644 index 0000000000000..8d1beacf272d2 --- /dev/null +++ b/extensions/jgit/deployment/src/main/java/io/quarkus/jgit/deployment/JGitProcessor.java @@ -0,0 +1,51 @@ +package io.quarkus.jgit.deployment; + +import java.util.Arrays; +import java.util.List; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; + +class JGitProcessor { + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FeatureBuildItem.JGIT); + } + + @BuildStep + ExtensionSslNativeSupportBuildItem activateSslNativeSupport() { + return new ExtensionSslNativeSupportBuildItem(FeatureBuildItem.JGIT); + } + + @BuildStep + ReflectiveClassBuildItem reflection() { + //Classes that use reflection + return new ReflectiveClassBuildItem(true, true, + "org.eclipse.jgit.api.MergeCommand$FastForwardMode", + "org.eclipse.jgit.api.MergeCommand$FastForwardMode$Merge", + "org.eclipse.jgit.internal.JGitText", + "org.eclipse.jgit.lib.CoreConfig$AutoCRLF", + "org.eclipse.jgit.lib.CoreConfig$CheckStat", + "org.eclipse.jgit.lib.CoreConfig$EOL", + "org.eclipse.jgit.lib.CoreConfig$EolStreamType", + "org.eclipse.jgit.lib.CoreConfig$HideDotFiles", + "org.eclipse.jgit.lib.CoreConfig$SymLinks"); + } + + @BuildStep + List runtimeInitializedClasses() { + return Arrays.asList( + new RuntimeInitializedClassBuildItem("org.eclipse.jgit.transport.HttpAuthMethod$Digest"), + new RuntimeInitializedClassBuildItem("org.eclipse.jgit.lib.GpgSigner")); + } + + @BuildStep + NativeImageResourceBundleBuildItem includeResourceBundle() { + return new NativeImageResourceBundleBuildItem("org.eclipse.jgit.internal.JGitText"); + } +} diff --git a/extensions/jgit/runtime/pom.xml b/extensions/jgit/runtime/pom.xml index 620dff6e6e1f5..9ae1132e76bc6 100644 --- a/extensions/jgit/runtime/pom.xml +++ b/extensions/jgit/runtime/pom.xml @@ -18,6 +18,10 @@ com.oracle.substratevm svm + + io.quarkus + quarkus-jsch + org.eclipse.jgit org.eclipse.jgit diff --git a/extensions/jsch/deployment/pom.xml b/extensions/jsch/deployment/pom.xml new file mode 100644 index 0000000000000..8d49d3e7a269e --- /dev/null +++ b/extensions/jsch/deployment/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + io.quarkus + quarkus-jsch-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-jsch-deployment + Quarkus - JSch - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-jsch + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/jgit/deployment/src/main/java/io/quarkus/jgit/runtime/deployment/JGitProcessor.java b/extensions/jsch/deployment/src/main/java/io/quarkus/jsch/deployment/JSchProcessor.java similarity index 70% rename from extensions/jgit/deployment/src/main/java/io/quarkus/jgit/runtime/deployment/JGitProcessor.java rename to extensions/jsch/deployment/src/main/java/io/quarkus/jsch/deployment/JSchProcessor.java index b050ad6a1b1da..632dca9933773 100644 --- a/extensions/jgit/deployment/src/main/java/io/quarkus/jgit/runtime/deployment/JGitProcessor.java +++ b/extensions/jsch/deployment/src/main/java/io/quarkus/jsch/deployment/JSchProcessor.java @@ -1,22 +1,28 @@ -package io.quarkus.jgit.runtime.deployment; +package io.quarkus.jsch.deployment; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.EnableAllSecurityServicesBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.jsch.runtime.PortWatcherRunTime; -class JGitProcessor { +class JSchProcessor { @BuildStep - FeatureBuildItem feature() { - return new FeatureBuildItem(FeatureBuildItem.JGIT); + EnableAllSecurityServicesBuildItem enableAllSecurityServices() { + return new EnableAllSecurityServicesBuildItem(); } @BuildStep - ExtensionSslNativeSupportBuildItem activateSslNativeSupport() { - return new ExtensionSslNativeSupportBuildItem(FeatureBuildItem.JGIT); + ExtensionSslNativeSupportBuildItem sslNativeSupport() { + return new ExtensionSslNativeSupportBuildItem(FeatureBuildItem.JSCH); + } + + @BuildStep + RuntimeInitializedClassBuildItem runtimeInitialized() { + return new RuntimeInitializedClassBuildItem(PortWatcherRunTime.class.getName()); } @BuildStep @@ -71,25 +77,6 @@ ReflectiveClassBuildItem reflection() { "com.jcraft.jsch.UserAuthKeyboardInteractive", "com.jcraft.jsch.UserAuthNone", "com.jcraft.jsch.UserAuthPassword", - "com.jcraft.jsch.UserAuthPublicKey", - "org.eclipse.jgit.api.MergeCommand$FastForwardMode", - "org.eclipse.jgit.api.MergeCommand$FastForwardMode$Merge", - "org.eclipse.jgit.internal.JGitText", - "org.eclipse.jgit.lib.CoreConfig$AutoCRLF", - "org.eclipse.jgit.lib.CoreConfig$CheckStat", - "org.eclipse.jgit.lib.CoreConfig$EOL", - "org.eclipse.jgit.lib.CoreConfig$EolStreamType", - "org.eclipse.jgit.lib.CoreConfig$HideDotFiles", - "org.eclipse.jgit.lib.CoreConfig$SymLinks"); - } - - @BuildStep - RuntimeInitializedClassBuildItem lazyDigest() { - return new RuntimeInitializedClassBuildItem("org.eclipse.jgit.transport.HttpAuthMethod$Digest"); - } - - @BuildStep - NativeImageResourceBundleBuildItem includeResourceBundle() { - return new NativeImageResourceBundleBuildItem("org.eclipse.jgit.internal.JGitText"); + "com.jcraft.jsch.UserAuthPublicKey"); } } diff --git a/extensions/jsch/pom.xml b/extensions/jsch/pom.xml new file mode 100644 index 0000000000000..4cd0b125115df --- /dev/null +++ b/extensions/jsch/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + + quarkus-jsch-parent + Quarkus - JSch - Parent + + pom + + deployment + runtime + + diff --git a/extensions/jsch/runtime/pom.xml b/extensions/jsch/runtime/pom.xml new file mode 100644 index 0000000000000..2b6bf18b4ed13 --- /dev/null +++ b/extensions/jsch/runtime/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + io.quarkus + quarkus-jsch-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-jsch + Quarkus - JSch - Runtime + JSch is a pure Java implementation of SSH2 and allows you to connect to an sshd server and use port forwarding, X11 forwarding, file transfer, etc. + + + + com.oracle.substratevm + svm + + + com.jcraft + jsch + + + com.jcraft + jzlib + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/jsch/runtime/src/main/java/io/quarkus/jsch/runtime/PortWatcherRunTime.java b/extensions/jsch/runtime/src/main/java/io/quarkus/jsch/runtime/PortWatcherRunTime.java new file mode 100644 index 0000000000000..c31ffe81d82a3 --- /dev/null +++ b/extensions/jsch/runtime/src/main/java/io/quarkus/jsch/runtime/PortWatcherRunTime.java @@ -0,0 +1,16 @@ +package io.quarkus.jsch.runtime; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class PortWatcherRunTime { + + public static InetAddress anyLocalAddress; + + static { + try { + anyLocalAddress = InetAddress.getByName("0.0.0.0"); + } catch (UnknownHostException e) { + } + } +} diff --git a/extensions/jsch/runtime/src/main/java/io/quarkus/jsch/runtime/PortWatcherSubstitutions.java b/extensions/jsch/runtime/src/main/java/io/quarkus/jsch/runtime/PortWatcherSubstitutions.java new file mode 100644 index 0000000000000..3640228b26021 --- /dev/null +++ b/extensions/jsch/runtime/src/main/java/io/quarkus/jsch/runtime/PortWatcherSubstitutions.java @@ -0,0 +1,27 @@ +package io.quarkus.jsch.runtime; + +import java.net.InetAddress; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.InjectAccessors; +import com.oracle.svm.core.annotate.TargetClass; + +/* + * The following substitution is required because of a new restriction in GraalVM 19.3.0 that prohibits the presence of + * java.net.Inet4Address in the image heap. Each field annotated with @InjectAccessors is lazily recomputed at runtime on first + * access while PortWatcher.class can still be initialized during the native image build. + */ +@TargetClass(className = "com.jcraft.jsch.PortWatcher") +final class Target_com_jcraft_jsch_PortWatcher { + + @Alias + @InjectAccessors(AnyLocalAddressAccessor.class) + private static InetAddress anyLocalAddress; +} + +final class AnyLocalAddressAccessor { + + static InetAddress get() { + return PortWatcherRunTime.anyLocalAddress; + } +} diff --git a/extensions/jsch/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/jsch/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..8063b678d7ca1 --- /dev/null +++ b/extensions/jsch/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +name: "JSch" +metadata: + keywords: + - "jsch" + - "ssh" + - "ssh2" + categories: + - "miscellaneous" + status: "stable" + unlisted: "true" \ No newline at end of file diff --git a/extensions/kafka-client/deployment/pom.xml b/extensions/kafka-client/deployment/pom.xml index 51ca51ab83dfc..240d59931c584 100644 --- a/extensions/kafka-client/deployment/pom.xml +++ b/extensions/kafka-client/deployment/pom.xml @@ -22,6 +22,10 @@ io.quarkus quarkus-kafka-client + + io.quarkus + quarkus-smallrye-health-spi + io.quarkus quarkus-junit5-internal diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaBuildTimeConfig.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaBuildTimeConfig.java new file mode 100644 index 0000000000000..58e07f1558acd --- /dev/null +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaBuildTimeConfig.java @@ -0,0 +1,16 @@ +package io.quarkus.kafka.client.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "kafka", phase = ConfigPhase.BUILD_TIME) +public class KafkaBuildTimeConfig { + /** + * Whether or not an health check is published in case the smallrye-health extension is present. + *

+ * If you enable the health check, you must specify the `kafka.bootstrap.servers` property. + */ + @ConfigItem(name = "health.enabled", defaultValue = "false") + public boolean healthEnabled; +} diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index 3a1468169b202..cd7f684695632 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -2,7 +2,6 @@ import java.util.Arrays; import java.util.Collection; -import java.util.zip.Checksum; import org.apache.kafka.clients.consumer.RangeAssignor; import org.apache.kafka.clients.consumer.RoundRobinAssignor; @@ -34,21 +33,16 @@ import org.jboss.jandex.DotName; import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.JniBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.ClassOutput; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.kafka.client.serialization.JsonbDeserializer; import io.quarkus.kafka.client.serialization.JsonbSerializer; import io.quarkus.kafka.client.serialization.ObjectMapperDeserializer; import io.quarkus.kafka.client.serialization.ObjectMapperSerializer; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; public class KafkaProcessor { @@ -75,7 +69,6 @@ public class KafkaProcessor { StringDeserializer.class, FloatDeserializer.class, }; - static final String TARGET_JAVA_9_CHECKSUM_FACTORY = "io.quarkus.kafka.client.generated.Target_Java9ChecksumFactory"; @BuildStep public void build(CombinedIndexBuildItem indexBuildItem, BuildProducer reflectiveClass, @@ -116,28 +109,9 @@ public void build(CombinedIndexBuildItem indexBuildItem, BuildProducer producer) { - // make our own class output to ensure that our step is run. - ClassOutput classOutput = new GeneratedClassGizmoAdaptor(producer, false); - try (ClassCreator cc = ClassCreator.builder().className(TARGET_JAVA_9_CHECKSUM_FACTORY) - .classOutput(classOutput).setFinal(true).superClass(Object.class).build()) { - - cc.addAnnotation("com/oracle/svm/core/annotate/TargetClass").addValue("className", - "org.apache.kafka.common.utils.Crc32C$Java9ChecksumFactory"); - cc.addAnnotation("com/oracle/svm/core/annotate/Substitute"); - - try (MethodCreator mc = cc.getMethodCreator("create", Checksum.class)) { - mc.addAnnotation("com/oracle/svm/core/annotate/Substitute"); - mc.returnValue(mc.newInstance(MethodDescriptor.ofConstructor("java.util.zip.CRC32C"))); - } - } + HealthBuildItem addHealthCheck(KafkaBuildTimeConfig buildTimeConfig) { + return new HealthBuildItem("io.quarkus.kafka.client.health.KafkaHealthCheck", + buildTimeConfig.healthEnabled, "kafka"); } } diff --git a/extensions/kafka-client/runtime/pom.xml b/extensions/kafka-client/runtime/pom.xml index 08e2a0fa2e26b..382b3ecc0dbd8 100644 --- a/extensions/kafka-client/runtime/pom.xml +++ b/extensions/kafka-client/runtime/pom.xml @@ -28,6 +28,11 @@ quarkus-jackson true + + io.quarkus + quarkus-smallrye-health + true + org.apache.kafka diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/health/KafkaHealthCheck.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/health/KafkaHealthCheck.java new file mode 100644 index 0000000000000..ac717b2d03bca --- /dev/null +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/health/KafkaHealthCheck.java @@ -0,0 +1,57 @@ +package io.quarkus.kafka.client.health; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; + +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.apache.kafka.common.Node; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; + +@Readiness +@ApplicationScoped +public class KafkaHealthCheck implements HealthCheck { + + @ConfigProperty(name = "quarkus.kafka.bootstrap-servers", defaultValue = "localhost:9092") + private String bootstrapServers; + + private AdminClient client; + + @PostConstruct + void init() { + Map conf = new HashMap<>(); + conf.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + conf.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, "5000"); + client = AdminClient.create(conf); + } + + @PreDestroy + void stop() { + client.close(); + } + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named("Kafka connection health check").up(); + try { + StringBuilder nodes = new StringBuilder(); + for (Node node : client.describeCluster().nodes().get()) { + if (nodes.length() > 0) { + nodes.append(','); + } + nodes.append(node.host()).append(':').append(node.port()); + } + return builder.withData("nodes", nodes.toString()).build(); + } catch (Exception e) { + return builder.down().withData("reason", e.getMessage()).build(); + } + } +} diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfig.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfig.java new file mode 100644 index 0000000000000..4172631cc99f8 --- /dev/null +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfig.java @@ -0,0 +1,21 @@ +package io.quarkus.kafka.client.runtime; + +import java.net.InetSocketAddress; +import java.util.List; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * For now, this is not used except to avoid a warning for the health check. + */ +@ConfigRoot(name = "kafka", phase = ConfigPhase.RUN_TIME) +public class KafkaRuntimeConfig { + + /** + * A comma-separated list of host:port pairs identifying the Kafka bootstrap server(s) + */ + @ConfigItem(defaultValue = "localhost:9012") + public List bootstrapServers; +} diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/Crc32CSubstitutions.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/Crc32CSubstitutions.java new file mode 100644 index 0000000000000..b591bf9120e2b --- /dev/null +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/Crc32CSubstitutions.java @@ -0,0 +1,35 @@ +package io.quarkus.kafka.client.runtime.graal; + +import java.lang.invoke.MethodHandle; +import java.util.zip.Checksum; + +import org.apache.kafka.common.utils.Crc32C; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jdk.JDK11OrLater; + +/** + * The following substitution replaces the usage of {@code MethodHandle} in {@code Java9ChecksumFactory} with a plain + * constructor invocation when run under GraalVM. This is necessary because the native image generator does not support method + * handles. + */ +@TargetClass(value = Crc32C.class, innerClass = "Java9ChecksumFactory", onlyWith = JDK11OrLater.class) +final class Target_org_apache_kafka_common_utils_Crc32C_Java9ChecksumFactory { + + @Alias + @RecomputeFieldValue(kind = Kind.Reset) + private static MethodHandle CONSTRUCTOR; + + @Substitute + public Checksum create() { + try { + return (Checksum) Class.forName("java.util.zip.CRC32C").getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/SubstituteSnappy.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/SubstituteSnappy.java index 32ecec78610f9..ca0d6d2410424 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/SubstituteSnappy.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/SubstituteSnappy.java @@ -67,7 +67,7 @@ public static CompressionType forId(int id) { final class RemoveJMXAccess { @Substitute - public static synchronized void registerAppInfo(String prefix, String id, Metrics metrics) { + public static synchronized void registerAppInfo(String prefix, String id, Metrics metrics, long nowMs) { } diff --git a/extensions/kafka-streams/deployment/pom.xml b/extensions/kafka-streams/deployment/pom.xml index 45c7e1e03d8a9..b59e7fb29cdc4 100644 --- a/extensions/kafka-streams/deployment/pom.xml +++ b/extensions/kafka-streams/deployment/pom.xml @@ -30,6 +30,10 @@ io.quarkus quarkus-kafka-streams + + io.quarkus + quarkus-smallrye-health-spi + diff --git a/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsBuildTimeConfig.java b/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsBuildTimeConfig.java new file mode 100644 index 0000000000000..f3aeaa8fc76d7 --- /dev/null +++ b/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsBuildTimeConfig.java @@ -0,0 +1,15 @@ +package io.quarkus.kafka.streams.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "kafka-streams", phase = ConfigPhase.BUILD_TIME) +public class KafkaStreamsBuildTimeConfig { + + /** + * Whether or not a health check is published in case the smallrye-health extension is present (defaults to true). + */ + @ConfigItem(name = "health.enabled", defaultValue = "true") + public boolean healthEnabled; +} diff --git a/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsHotReplacementSetup.java b/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsHotReplacementSetup.java index 96ea504dfc856..892f0c0ceb23c 100644 --- a/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsHotReplacementSetup.java +++ b/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsHotReplacementSetup.java @@ -12,7 +12,7 @@ public class KafkaStreamsHotReplacementSetup implements HotReplacementSetup { private static final long TWO_SECONDS = 2000; private HotReplacementContext context; - private long nextUpdate; + private volatile long nextUpdate; private final Executor executor = Executors.newSingleThreadExecutor(); @Override diff --git a/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsProcessor.java b/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsProcessor.java index dfa9a6085f470..6a7c2f204d35b 100644 --- a/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsProcessor.java +++ b/extensions/kafka-streams/deployment/src/main/java/io/quarkus/kafka/streams/deployment/KafkaStreamsProcessor.java @@ -33,6 +33,7 @@ import io.quarkus.kafka.streams.runtime.KafkaStreamsRuntimeConfig; import io.quarkus.kafka.streams.runtime.KafkaStreamsTopologyManager; import io.quarkus.runtime.LaunchMode; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; class KafkaStreamsProcessor { @@ -196,4 +197,16 @@ void configureAndLoadRocksDb(KafkaStreamsRecorder recorder, KafkaStreamsRuntimeC AdditionalBeanBuildItem registerBean() { return AdditionalBeanBuildItem.unremovableOf(KafkaStreamsTopologyManager.class); } + + @BuildStep + void addHealthChecks(KafkaStreamsBuildTimeConfig buildTimeConfig, BuildProducer healthChecks) { + healthChecks.produce( + new HealthBuildItem( + "io.quarkus.kafka.streams.runtime.health.KafkaStreamsTopicsHealthCheck", + buildTimeConfig.healthEnabled, "kafka-streams")); + healthChecks.produce( + new HealthBuildItem( + "io.quarkus.kafka.streams.runtime.health.KafkaStreamsStateHealthCheck", + buildTimeConfig.healthEnabled, "kafka-streams")); + } } diff --git a/extensions/kafka-streams/runtime/pom.xml b/extensions/kafka-streams/runtime/pom.xml index df683d160a149..2be409ef18ac1 100644 --- a/extensions/kafka-streams/runtime/pom.xml +++ b/extensions/kafka-streams/runtime/pom.xml @@ -34,6 +34,11 @@ com.oracle.substratevm svm + + io.quarkus + quarkus-smallrye-health + true + diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/KafkaStreamsTopologyManager.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/KafkaStreamsTopologyManager.java index b1ae0f5af5ea6..ba32e5defae3b 100644 --- a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/KafkaStreamsTopologyManager.java +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/KafkaStreamsTopologyManager.java @@ -2,6 +2,7 @@ import java.net.InetSocketAddress; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -50,6 +51,7 @@ public class KafkaStreamsTopologyManager { private KafkaStreamsRuntimeConfig runtimeConfig; private Instance topology; private Properties properties; + private Map adminClientConfig; KafkaStreamsTopologyManager() { executor = null; @@ -105,10 +107,11 @@ void onStart(@Observes StartupEvent ev) { Properties streamsProperties = getStreamsProperties(properties, bootstrapServersConfig, runtimeConfig); streams = new KafkaStreams(topology.get(), streamsProperties); + adminClientConfig = getAdminClientConfig(bootstrapServersConfig); executor.execute(() -> { try { - waitForTopicsToBeCreated(runtimeConfig.getTrimmedTopics(), bootstrapServersConfig); + waitForTopicsToBeCreated(runtimeConfig.getTrimmedTopics()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; @@ -131,21 +134,9 @@ public KafkaStreams getStreams() { return streams; } - private void waitForTopicsToBeCreated(Collection topicsToAwait, String bootstrapServersConfig) + private void waitForTopicsToBeCreated(Collection topicsToAwait) throws InterruptedException { - final Map config = new HashMap<>(); - config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServersConfig); - // include other AdminClientConfig(s) that have been configured - for (final String knownAdminClientConfig : AdminClientConfig.configNames()) { - // give preference to admin. first - if (properties.containsKey(StreamsConfig.ADMIN_CLIENT_PREFIX + knownAdminClientConfig)) { - config.put(knownAdminClientConfig, properties.get(StreamsConfig.ADMIN_CLIENT_PREFIX + knownAdminClientConfig)); - } else if (properties.containsKey(knownAdminClientConfig)) { - config.put(knownAdminClientConfig, properties.get(knownAdminClientConfig)); - } - } - - try (AdminClient adminClient = AdminClient.create(config)) { + try (AdminClient adminClient = AdminClient.create(adminClientConfig)) { while (true) { try { ListTopicsResult topics = adminClient.listTopics(); @@ -155,7 +146,7 @@ private void waitForTopicsToBeCreated(Collection topicsToAwait, String b LOGGER.debug("All expected topics created"); return; } else { - HashSet missing = new HashSet<>(topicsToAwait); + Set missing = new HashSet<>(topicsToAwait); missing.removeAll(topicNames); LOGGER.debug("Waiting for topic(s) to be created: " + missing); } @@ -168,6 +159,42 @@ private void waitForTopicsToBeCreated(Collection topicsToAwait, String b } } + public Set getMissingTopics(Collection topicsToCheck) + throws InterruptedException { + HashSet missing = new HashSet<>(topicsToCheck); + + try (AdminClient adminClient = AdminClient.create(adminClientConfig)) { + ListTopicsResult topics = adminClient.listTopics(); + Set topicNames = topics.names().get(10, TimeUnit.SECONDS); + + if (topicNames.containsAll(topicsToCheck)) { + return Collections.EMPTY_SET; + } else { + missing.removeAll(topicNames); + } + } catch (ExecutionException | TimeoutException e) { + LOGGER.error("Failed to get topic names from broker", e); + } + + return missing; + } + + private Map getAdminClientConfig(String bootstrapServersConfig) { + Map adminClientConfig = new HashMap<>(); + adminClientConfig.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServersConfig); + // include other AdminClientConfig(s) that have been configured + for (final String knownAdminClientConfig : AdminClientConfig.configNames()) { + // give preference to admin. first + if (properties.containsKey(StreamsConfig.ADMIN_CLIENT_PREFIX + knownAdminClientConfig)) { + adminClientConfig.put(knownAdminClientConfig, + properties.get(StreamsConfig.ADMIN_CLIENT_PREFIX + knownAdminClientConfig)); + } else if (properties.containsKey(knownAdminClientConfig)) { + adminClientConfig.put(knownAdminClientConfig, properties.get(knownAdminClientConfig)); + } + } + return adminClientConfig; + } + public void setRuntimeConfig(KafkaStreamsRuntimeConfig runtimeConfig) { this.runtimeConfig = runtimeConfig; } diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/health/KafkaStreamsStateHealthCheck.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/health/KafkaStreamsStateHealthCheck.java new file mode 100644 index 0000000000000..de5d8cdc434a9 --- /dev/null +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/health/KafkaStreamsStateHealthCheck.java @@ -0,0 +1,33 @@ +package io.quarkus.kafka.streams.runtime.health; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.kafka.streams.KafkaStreams; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Liveness; + +import io.quarkus.kafka.streams.runtime.KafkaStreamsTopologyManager; + +@Liveness +@ApplicationScoped +public class KafkaStreamsStateHealthCheck implements HealthCheck { + + @Inject + private KafkaStreamsTopologyManager manager; + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("Kafka Streams state health check"); + try { + KafkaStreams.State state = manager.getStreams().state(); + responseBuilder.state(state.isRunning()) + .withData("state", state.name()); + } catch (Exception e) { + responseBuilder.down().withData("technical_error", e.getMessage()); + } + return responseBuilder.build(); + } +} diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/health/KafkaStreamsTopicsHealthCheck.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/health/KafkaStreamsTopicsHealthCheck.java new file mode 100644 index 0000000000000..c6eb07be0a10a --- /dev/null +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/health/KafkaStreamsTopicsHealthCheck.java @@ -0,0 +1,67 @@ +package io.quarkus.kafka.streams.runtime.health; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; +import org.jboss.logging.Logger; + +import io.quarkus.kafka.streams.runtime.KafkaStreamsTopologyManager; + +@Readiness +@ApplicationScoped +public class KafkaStreamsTopicsHealthCheck implements HealthCheck { + + private static final Logger LOGGER = Logger.getLogger(KafkaStreamsTopicsHealthCheck.class.getName()); + + @ConfigProperty(name = "quarkus.kafka-streams.topics") + public List topics; + + // @ConfigProperty(name = "quarkus.kafka-streams.bootstrap-servers") + // public List bootstrapServers; + + @Inject + private KafkaStreamsTopologyManager manager; + + // private String commaSeparatedBootstrapServersConfig; + private List trimmedTopics; + + @PostConstruct + public void init() { + // commaSeparatedBootstrapServersConfig = String.join(",", bootstrapServers); + trimmedTopics = topics.stream().map(String::trim).collect(Collectors.toList()); + } + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named("Kafka Streams topics health check").up(); + + try { + Set missingTopics = manager.getMissingTopics(trimmedTopics); + List availableTopics = new ArrayList<>(trimmedTopics); + availableTopics.removeAll(missingTopics); + + if (!availableTopics.isEmpty()) { + builder.withData("available_topics", String.join(",", availableTopics)); + } + if (!missingTopics.isEmpty()) { + builder.down().withData("missing_topics", String.join(",", missingTopics)); + } + } catch (InterruptedException e) { + LOGGER.error("error when retrieving missing topics", e); + builder.down().withData("technical_error", e.getMessage()); + } + + return builder.build(); + } +} diff --git a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java index 7de9e80705c7f..5353dc95221cd 100644 --- a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java +++ b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.keycloak.pep.deployment; +import java.util.Map; + import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.deployment.annotations.BuildStep; @@ -11,7 +13,9 @@ import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerConfig; import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerRecorder; import io.quarkus.oidc.OIDCException; +import io.quarkus.oidc.runtime.OidcBuildTimeConfig; import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem; public class KeycloakPolicyEnforcerBuildStep { @@ -20,6 +24,36 @@ FeatureBuildItem featureBuildItem() { return new FeatureBuildItem(FeatureBuildItem.KEYCLOAK_AUTHORIZATION); } + @BuildStep + RequireBodyHandlerBuildItem requireBody(KeycloakPolicyEnforcerConfig config) { + if (config.policyEnforcer.enable) { + if (isBodyClaimInformationPointDefined(config.policyEnforcer.claimInformationPoint.simpleConfig)) { + return new RequireBodyHandlerBuildItem(); + } + for (KeycloakPolicyEnforcerConfig.KeycloakConfigPolicyEnforcer.PathConfig path : config.policyEnforcer.paths + .values()) { + if (isBodyClaimInformationPointDefined(path.claimInformationPoint.simpleConfig)) { + return new RequireBodyHandlerBuildItem(); + } + } + } + return null; + } + + private boolean isBodyClaimInformationPointDefined(Map> claims) { + for (Map.Entry> entry : claims.entrySet()) { + Map value = entry.getValue(); + + for (String nestedValue : value.values()) { + if (nestedValue.contains("request.body")) { + return true; + } + } + } + + return false; + } + @BuildStep public AdditionalBeanBuildItem beans(KeycloakPolicyEnforcerConfig config) { if (config.policyEnforcer.enable) { @@ -36,13 +70,13 @@ EnableAllSecurityServicesBuildItem security() { @Record(ExecutionTime.RUNTIME_INIT) @BuildStep - public void setup(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config, KeycloakPolicyEnforcerRecorder recorder, - BeanContainerBuildItem bc) { - if (!oidcConfig.getApplicationType().equals(OidcConfig.ApplicationType.SERVICE)) { - throw new OIDCException("Application type [" + oidcConfig.getApplicationType() + "] not supported"); + public void setup(OidcBuildTimeConfig buildTimeConfig, KeycloakPolicyEnforcerConfig keycloakConfig, + OidcConfig runTimeConfig, KeycloakPolicyEnforcerRecorder recorder, BeanContainerBuildItem bc) { + if (!buildTimeConfig.applicationType.equals(OidcBuildTimeConfig.ApplicationType.SERVICE)) { + throw new OIDCException("Application type [" + buildTimeConfig.applicationType + "] not supported"); } - if (config.policyEnforcer.enable) { - recorder.setup(oidcConfig, config, bc.getValue()); + if (keycloakConfig.policyEnforcer.enable) { + recorder.setup(runTimeConfig, keycloakConfig, bc.getValue()); } } } diff --git a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java index 9bc5340dc1786..584b3fd9e79a2 100644 --- a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java +++ b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java @@ -13,6 +13,7 @@ import org.keycloak.jose.jws.JWSHeader; import org.keycloak.json.StringListMapDeserializer; import org.keycloak.json.StringOrArrayDeserializer; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; @@ -31,6 +32,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; public class KeycloakReflectionBuildStep { @@ -60,7 +62,8 @@ public void registerReflectionItems(BuildProducer refl ScopeRepresentation.class.getName(), ResourceOwnerRepresentation.class.getName(), StringListMapDeserializer.class.getName(), - StringOrArrayDeserializer.class.getName())); + StringOrArrayDeserializer.class.getName(), + OIDCConfigurationRepresentation.class.getName())); } @BuildStep @@ -74,4 +77,14 @@ public void registerServiceProviders(BuildProducer ser ClaimsInformationPointProviderFactory.class.getName())); } + + @BuildStep + public void runtimeInit(BuildProducer runtimeInit) { + runtimeInit.produce(new RuntimeInitializedClassBuildItem("org.keycloak.common.util.BouncyIntegration")); + runtimeInit.produce(new RuntimeInitializedClassBuildItem("org.keycloak.common.util.PemUtils")); + runtimeInit.produce(new RuntimeInitializedClassBuildItem("org.keycloak.common.util.DerUtils")); + runtimeInit.produce(new RuntimeInitializedClassBuildItem("org.keycloak.common.util.KeystoreUtil")); + runtimeInit.produce(new RuntimeInitializedClassBuildItem("org.keycloak.common.util.CertificateUtils")); + runtimeInit.produce(new RuntimeInitializedClassBuildItem("org.keycloak.common.util.OCSPUtils")); + } } diff --git a/extensions/keycloak-authorization/runtime/pom.xml b/extensions/keycloak-authorization/runtime/pom.xml index 23d49a7f4f158..9c3f164056aa0 100644 --- a/extensions/keycloak-authorization/runtime/pom.xml +++ b/extensions/keycloak-authorization/runtime/pom.xml @@ -12,6 +12,7 @@ quarkus-keycloak-authorization Quarkus - Keycloak Authorization - Runtime + Policy enforcer using Keycloak-managed permissions to control access to protected resources diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java index 3c65573fda95e..5a9892d50133d 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java @@ -20,6 +20,7 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.oidc.runtime.OidcTenantConfig; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; @@ -87,7 +88,7 @@ public CompletionStage apply(Permission permission) { public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) { AdapterConfig adapterConfig = new AdapterConfig(); - String authServerUrl = oidcConfig.getAuthServerUrl(); + String authServerUrl = oidcConfig.defaultTenant.getAuthServerUrl().get(); try { adapterConfig.setRealm(authServerUrl.substring(authServerUrl.lastIndexOf('/') + 1)); @@ -96,8 +97,8 @@ public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) { throw new RuntimeException("Failed to parse the realm name.", cause); } - adapterConfig.setResource(oidcConfig.getClientId().get()); - adapterConfig.setCredentials(getCredentials(oidcConfig)); + adapterConfig.setResource(oidcConfig.defaultTenant.getClientId().get()); + adapterConfig.setCredentials(getCredentials(oidcConfig.defaultTenant)); PolicyEnforcerConfig enforcerConfig = getPolicyEnforcerConfig(config, adapterConfig); @@ -111,7 +112,7 @@ public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) { new PolicyEnforcer(KeycloakDeploymentBuilder.build(adapterConfig), adapterConfig)); } - private Map getCredentials(OidcConfig oidcConfig) { + private Map getCredentials(OidcTenantConfig oidcConfig) { Map credentials = new HashMap<>(); Optional clientSecret = oidcConfig.getCredentials().getSecret(); diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java index 9f6545124b69f..118ed1247c348 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerConfig.java @@ -41,13 +41,13 @@ public static class KeycloakConfigPolicyEnforcer { * Specifies how policies are enforced. */ @ConfigItem(defaultValue = "ENFORCING") - String enforcementMode; + public String enforcementMode; /** * Specifies the paths to protect. */ @ConfigItem - Map paths; + public Map paths; /** * Defines how the policy enforcer should track associations between paths in your application and resources defined in @@ -56,7 +56,7 @@ public static class KeycloakConfigPolicyEnforcer { * protected resources */ @ConfigItem - PathCacheConfig pathCache; + public PathCacheConfig pathCache; /** * Specifies how the adapter should fetch the server for resources associated with paths in your application. If true, @@ -65,14 +65,14 @@ public static class KeycloakConfigPolicyEnforcer { * enforcer is going to fetch resources on-demand accordingly with the path being requested */ @ConfigItem(defaultValue = "true") - boolean lazyLoadPaths; + public boolean lazyLoadPaths; /** * Defines a set of one or more claims that must be resolved and pushed to the Keycloak server in order to make these * claims available to policies */ @ConfigItem - ClaimInformationPointConfig claimInformationPoint; + public ClaimInformationPointConfig claimInformationPoint; /** * Specifies how scopes should be mapped to HTTP methods. If set to true, the policy enforcer will use the HTTP method @@ -80,7 +80,7 @@ public static class KeycloakConfigPolicyEnforcer { * the current request to check whether or not access should be granted */ @ConfigItem - boolean httpMethodAsScope; + public boolean httpMethodAsScope; @ConfigGroup public static class PathConfig { @@ -89,13 +89,13 @@ public static class PathConfig { * The name of a resource on the server that is to be associated with a given path */ @ConfigItem - Optional name; + public Optional name; /** * A URI relative to the application’s context path that should be protected by the policy enforcer */ @ConfigItem - Optional path; + public Optional path; /** * The HTTP methods (for example, GET, POST, PATCH) to protect and how they are associated with the scopes for a @@ -103,14 +103,14 @@ public static class PathConfig { * resource in the server */ @ConfigItem - Map methods; + public Map methods; /** * Specifies how policies are enforced */ @DefaultConverter @ConfigItem(defaultValue = "ENFORCING") - PolicyEnforcerConfig.EnforcementMode enforcementMode; + public PolicyEnforcerConfig.EnforcementMode enforcementMode; /** * Defines a set of one or more claims that must be resolved and pushed to the Keycloak server in order to make @@ -118,7 +118,7 @@ public static class PathConfig { * claims available to policies */ @ConfigItem - ClaimInformationPointConfig claimInformationPoint; + public ClaimInformationPointConfig claimInformationPoint; } @ConfigGroup @@ -128,20 +128,20 @@ public static class MethodConfig { * The name of the HTTP method */ @ConfigItem - String method; + public String method; /** * An array of strings with the scopes associated with the method */ @ConfigItem - List scopes; + public List scopes; /** * A string referencing the enforcement mode for the scopes associated with a method */ @DefaultConverter @ConfigItem(defaultValue = "ALL") - PolicyEnforcerConfig.ScopeEnforcementMode scopesEnforcementMode; + public PolicyEnforcerConfig.ScopeEnforcementMode scopesEnforcementMode; } @ConfigGroup @@ -151,13 +151,13 @@ public static class PathCacheConfig { * Defines the time in milliseconds when the entry should be expired */ @ConfigItem(defaultValue = "1000") - int maxEntries = 1000; + public int maxEntries = 1000; /** * Defines the limit of entries that should be kept in the cache */ @ConfigItem(defaultValue = "30000") - long lifespan = 30000; + public long lifespan = 30000; } @ConfigGroup @@ -167,13 +167,13 @@ public static class ClaimInformationPointConfig { * */ @ConfigItem(name = ConfigItem.PARENT) - Map>> complexConfig; + public Map>> complexConfig; /** * */ @ConfigItem(name = ConfigItem.PARENT) - Map> simpleConfig; + public Map> simpleConfig; } } } diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java index 78a8b5fb934fa..c8766c1ec188a 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/VertxHttpFacade.java @@ -1,6 +1,5 @@ package io.quarkus.keycloak.pep.runtime; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; @@ -23,8 +22,10 @@ import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.VertxInputStream; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.impl.CookieImpl; @@ -107,7 +108,16 @@ public Cookie getCookie(String cookieName) { @Override public String getHeader(String name) { - return request.getHeader(name); + //TODO: this logic should be removed once KEYCLOAK-12412 is fixed + String value = request.getHeader(name); + + if (name.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE.toString())) { + if (value.indexOf(';') != -1) { + return value.substring(0, value.indexOf(';')); + } + } + + return value; } @Override @@ -122,7 +132,17 @@ public InputStream getInputStream() { @Override public InputStream getInputStream(boolean buffered) { - return new BufferedInputStream(new ByteArrayInputStream(routingContext.getBody().getBytes())); + try { + if (routingContext.getBody() != null) { + return new ByteArrayInputStream(routingContext.getBody().getBytes()); + } + if (routingContext.request().isEnded()) { + return new ByteArrayInputStream(new byte[0]); + } + return new VertxInputStream(routingContext); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override diff --git a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/JandexProtoGenerator.java b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/JandexProtoGenerator.java index db3b5db43680e..e3269fa22c410 100644 --- a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/JandexProtoGenerator.java +++ b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/JandexProtoGenerator.java @@ -1,5 +1,6 @@ package io.quarkus.kogito.deployment; +import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -71,9 +72,15 @@ protected ProtoMessage messageFromClass(Proto proto, ClassInfo clazz, IndexView for (FieldInfo pd : clazz.fields()) { + // ignore static and/or transient fields + if (Modifier.isStatic(pd.flags()) || Modifier.isTransient(pd.flags())) { + continue; + } + String fieldTypeString = pd.type().name().toString(); DotName fieldType = pd.type().name(); + String protoType; if (pd.type().kind() == Kind.PARAMETERIZED_TYPE) { fieldTypeString = "Collection"; @@ -83,11 +90,17 @@ protected ProtoMessage messageFromClass(Proto proto, ClassInfo clazz, IndexView + " uses collection without type information"); } fieldType = typeParameters.get(0).name(); + protoType = protoType(fieldType.toString()); + } else { + protoType = protoType(fieldTypeString); } - String protoType = protoType(fieldTypeString); if (protoType == null) { - ProtoMessage another = messageFromClass(proto, index.getClassByName(fieldType), index, packageName, + ClassInfo classInfo = index.getClassByName(fieldType); + if (classInfo == null) { + throw new IllegalStateException("Cannot find class info in jandex index for " + fieldType); + } + ProtoMessage another = messageFromClass(proto, classInfo, index, packageName, messageComment, fieldComment); protoType = another.getName(); } @@ -131,7 +144,7 @@ protected void generateModelClassProto(ClassInfo modelClazz, String targetDirect if (processId != null) { Proto modelProto = generate("@Indexed", - "@Field(store = Store.YES, analyze = Analyze.YES)", + INDEX_COMMENT, modelClazz.name().prefix().toString() + "." + processId, modelClazz, "import \"kogito-index.proto\";", "import \"kogito-types.proto\";", @@ -140,10 +153,8 @@ protected void generateModelClassProto(ClassInfo modelClazz, String targetDirect ProtoMessage modelMessage = modelProto.getMessages().stream().filter(msg -> msg.getName().equals(name)).findFirst() .orElseThrow(() -> new IllegalStateException("Unable to find model message")); - modelMessage.addField("repeated", "org.kie.kogito.index.model.ProcessInstanceMeta", "processInstances") - .setComment("@Field(store = Store.YES, analyze = Analyze.YES)"); - modelMessage.addField("repeated", "org.kie.kogito.index.model.UserTaskInstanceMeta", "userTasks") - .setComment("@Field(store = Store.YES, analyze = Analyze.YES)"); + modelMessage.addField("optional", "org.kie.kogito.index.model.KogitoMetadata", "metadata") + .setComment(INDEX_COMMENT); Path protoFilePath = Paths.get(targetDirectory, "classes", "/persistence/" + processId + ".proto"); diff --git a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoAssetsProcessor.java b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoAssetsProcessor.java index d775ff1414db2..4fd45d3ecf7cb 100644 --- a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoAssetsProcessor.java +++ b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoAssetsProcessor.java @@ -20,6 +20,8 @@ import org.drools.compiler.commons.jci.compilers.JavaCompilerSettings; import org.drools.compiler.compiler.io.memory.MemoryFileSystem; import org.drools.compiler.kproject.models.KieModuleModelImpl; +import org.drools.core.base.ClassFieldAccessorFactory; +import org.drools.modelcompiler.builder.GeneratedFile; import org.drools.modelcompiler.builder.JavaParserCompiler; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; @@ -32,7 +34,6 @@ import org.kie.internal.kogito.codegen.Generated; import org.kie.kogito.Model; import org.kie.kogito.codegen.ApplicationGenerator; -import org.kie.kogito.codegen.GeneratedFile; import org.kie.kogito.codegen.decision.DecisionCodegen; import org.kie.kogito.codegen.di.CDIDependencyInjectionAnnotator; import org.kie.kogito.codegen.process.ProcessCodegen; @@ -53,6 +54,7 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.index.IndexingUtil; import io.quarkus.runtime.LaunchMode; @@ -134,6 +136,11 @@ public List reflectiveDMNREST() { return result; } + @BuildStep + public RuntimeInitializedClassBuildItem runtimeInitializedClass() { + return new RuntimeInitializedClassBuildItem(ClassFieldAccessorFactory.class.getName()); + } + @BuildStep(loadsApplicationClasses = true) public void generateModel(ArchiveRootBuildItem root, BuildProducer generatedBeans, @@ -227,11 +234,11 @@ private CompilationResult compile(ArchiveRootBuildItem root, MemoryFileSystem tr String[] sources = new String[generatedFiles.size()]; int index = 0; for (GeneratedFile entry : generatedFiles) { - String generatedClassFile = entry.relativePath().replace("src/main/java/", ""); + String generatedClassFile = entry.getPath().replace("src/main/java/", ""); String fileName = toRuntimeSource(toClassName(generatedClassFile)); sources[index++] = fileName; - srcMfs.write(fileName, entry.contents()); + srcMfs.write(fileName, entry.getData()); String location = generatedClassesDir; if (launchMode == LaunchMode.DEVELOPMENT) { @@ -351,10 +358,10 @@ private void writeGeneratedFile(GeneratedFile f, String location) throws IOExcep if (location == null) { return; } - String generatedClassFile = f.relativePath().replace("src/main/java", ""); + String generatedClassFile = f.getPath().replace("src/main/java", ""); Files.write( pathOf(location, generatedClassFile), - f.contents()); + f.getData()); } private Path pathOf(String location, String end) { diff --git a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java index 341317d4e2449..10a83fdc53670 100644 --- a/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java +++ b/extensions/kogito/deployment/src/main/java/io/quarkus/kogito/deployment/KogitoCompilationProvider.java @@ -12,8 +12,8 @@ import java.util.Map; import java.util.Set; +import org.drools.modelcompiler.builder.GeneratedFile; import org.kie.kogito.codegen.ApplicationGenerator; -import org.kie.kogito.codegen.GeneratedFile; import org.kie.kogito.codegen.Generator; import org.kie.kogito.codegen.di.CDIDependencyInjectionAnnotator; @@ -44,8 +44,8 @@ public final void compile(Set filesToCompile, Context context) { HashSet generatedSourceFiles = new HashSet<>(); for (GeneratedFile file : generatedFiles) { - Path path = pathOf(outputDirectory.getPath(), file.relativePath()); - Files.write(path, file.contents()); + Path path = pathOf(outputDirectory.getPath(), file.getPath()); + Files.write(path, file.getData()); generatedSourceFiles.add(path.toFile()); } super.compile(generatedSourceFiles, context); diff --git a/extensions/kotlin/deployment/pom.xml b/extensions/kotlin/deployment/pom.xml index 5eda3361dc307..67622c54c0030 100644 --- a/extensions/kotlin/deployment/pom.xml +++ b/extensions/kotlin/deployment/pom.xml @@ -19,6 +19,10 @@ io.quarkus quarkus-kotlin + + io.quarkus + quarkus-jackson-spi + io.quarkus @@ -27,7 +31,7 @@ org.jetbrains.kotlin - kotlin-compiler-embeddable + kotlin-compiler diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/IsDataClassWithDefaultValuesPredicate.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/IsDataClassWithDefaultValuesPredicate.java new file mode 100644 index 0000000000000..c7d3f55d3aa7c --- /dev/null +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/IsDataClassWithDefaultValuesPredicate.java @@ -0,0 +1,38 @@ +package io.quarkus.kotlin.deployment; + +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.function.Predicate; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.MethodInfo; + +/** + * Tests whether a class is a data class (based on this answer: + * https://discuss.kotlinlang.org/t/detect-data-class-in-runtime/6155/2) + * and whether the class has default values for fields (default values leads to having multiple constructors in bytecode) + */ +public class IsDataClassWithDefaultValuesPredicate implements Predicate { + + @Override + public boolean test(ClassInfo classInfo) { + int ctorCount = 0; + boolean hasCopyMethod = false; + boolean hasStaticCopyMethod = false; + boolean hasComponent1Method = false; + List methods = classInfo.methods(); + for (MethodInfo method : methods) { + String methodName = method.name(); + if ("".equals(methodName)) { + ctorCount++; + } else if ("component1".equals(methodName) && Modifier.isFinal(method.flags())) { + hasComponent1Method = true; + } else if ("copy".equals(methodName) && Modifier.isFinal(method.flags())) { + hasCopyMethod = true; + } else if ("copy$default".equals(methodName) && Modifier.isStatic(method.flags())) { + hasStaticCopyMethod = true; + } + } + return ctorCount > 1 && hasComponent1Method && hasCopyMethod && hasStaticCopyMethod; + } +} diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java index 139f3041977a8..4a8350e1014a1 100644 --- a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java @@ -6,8 +6,11 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.jboss.logging.Logger; import org.jetbrains.kotlin.cli.common.ExitCode; import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments; import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation; @@ -20,6 +23,12 @@ public class KotlinCompilationProvider implements CompilationProvider { + private static final Logger log = Logger.getLogger(KotlinCompilationProvider.class); + + // see: https://github.com/JetBrains/kotlin/blob/v1.3.41/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/KotlinCompileMojoBase.java#L192 + private final static Pattern OPTION_PATTERN = Pattern.compile("([^:]+):([^=]+)=(.*)"); + private static final String KOTLIN_PACKAGE = "org.jetbrains.kotlin"; + @Override public Set handledExtensions() { return Collections.singleton(".kt"); @@ -28,6 +37,31 @@ public Set handledExtensions() { @Override public void compile(Set filesToCompile, Context context) { K2JVMCompilerArguments compilerArguments = new K2JVMCompilerArguments(); + compilerArguments.setJavaParameters(true); + if (context.getCompilePluginArtifacts() != null && !context.getCompilePluginArtifacts().isEmpty()) { + compilerArguments.setPluginClasspaths(context.getCompilePluginArtifacts().toArray(new String[0])); + } + if (context.getCompilerPluginOptions() != null && !context.getCompilerPluginOptions().isEmpty()) { + List sanitizedOptions = new ArrayList<>(context.getCompilerOptions().size()); + for (String rawOption : context.getCompilerPluginOptions()) { + Matcher matcher = OPTION_PATTERN.matcher(rawOption); + if (!matcher.matches()) { + log.warn("Kotlin compiler plugin option " + rawOption + " is invalid"); + } + + String pluginId = matcher.group(1); + if (!pluginId.contains(".")) { + // convert the plugin name to the plugin id by simply removing the dash and adding the kotlin package + // this seems to be the appropriate way of doing things for the plugins that were checked + pluginId = KOTLIN_PACKAGE + "." + pluginId.replace("-", ""); + } + String key = matcher.group(2); + String value = matcher.group(3); + sanitizedOptions.add("plugin:" + pluginId + ":" + key + "=" + value); + + compilerArguments.setPluginOptions(sanitizedOptions.toArray(new String[0])); + } + } compilerArguments.setClasspath( context.getClasspath().stream().map(File::getAbsolutePath).collect(Collectors.joining(File.pathSeparator))); compilerArguments.setDestination(context.getOutputDirectory().getAbsolutePath()); @@ -44,7 +78,7 @@ public void compile(Set filesToCompile, Context context) { } if (messageCollector.hasErrors()) { - throw new RuntimeException("Compilation failed" + String.join("\n", messageCollector.getErrors())); + throw new RuntimeException("Compilation failed. " + String.join("\n", messageCollector.getErrors())); } } @@ -71,8 +105,9 @@ public boolean hasErrors() { public void report(CompilerMessageSeverity severity, String s, CompilerMessageLocation location) { if (severity.isError()) { if ((location != null) && (location.getLineContent() != null)) { - errors.add(String.format("%s%n%s:%d:%d", location.getLineContent(), location.getPath(), location.getLine(), - location.getColumn())); + errors.add(String.format("%s%n%s:%d:%d%nReason: %s", location.getLineContent(), location.getPath(), + location.getLine(), + location.getColumn(), s)); } else { errors.add(s); } diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java index 3596ca27d9651..b50d11b76f878 100644 --- a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java @@ -1,12 +1,40 @@ package io.quarkus.kotlin.deployment; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassFinalFieldsWritablePredicateBuildItem; +import io.quarkus.jackson.spi.ClassPathJacksonModuleBuildItem; public class KotlinProcessor { + private static final String KOTLIN_JACKSON_MODULE = "com.fasterxml.jackson.module.kotlin.KotlinModule"; + @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(FeatureBuildItem.KOTLIN); } + + /* + * Register the Kotlin Jackson module if that has been added to the classpath + * Producing the BuildItem is entirely safe since if quarkus-jackson is not on the classpath + * the BuildItem will just be ignored + */ + @BuildStep + void registerKotlinJacksonModule(BuildProducer classPathJacksonModules) { + try { + Class.forName(KOTLIN_JACKSON_MODULE, false, Thread.currentThread().getContextClassLoader()); + classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(KOTLIN_JACKSON_MODULE)); + } catch (Exception ignored) { + } + } + + /** + * Kotlin data classes that have multiple constructors need to have their final fields writable, + * otherwise creating a instance of them with default values, fails in native mode + */ + @BuildStep + ReflectiveClassFinalFieldsWritablePredicateBuildItem dataClassPredicate() { + return new ReflectiveClassFinalFieldsWritablePredicateBuildItem(new IsDataClassWithDefaultValuesPredicate()); + } } diff --git a/extensions/kubernetes-client/runtime/pom.xml b/extensions/kubernetes-client/runtime/pom.xml index bc22cb63ae6e7..1d175d63e4d33 100644 --- a/extensions/kubernetes-client/runtime/pom.xml +++ b/extensions/kubernetes-client/runtime/pom.xml @@ -34,6 +34,10 @@ javax.annotation javax.annotation-api + + jakarta.xml.bind + jakarta.xml.bind-api + javax.xml.bind jaxb-api diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java index bc97f1f936835..c41980955be42 100644 --- a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientBuildConfig.java @@ -1,7 +1,6 @@ package io.quarkus.kubernetes.client.runtime; import java.time.Duration; -import java.util.List; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigItem; @@ -100,7 +99,7 @@ public class KubernetesClientBuildConfig { * By default there is no limit to the number of reconnect attempts */ @ConfigItem(defaultValue = "-1") // default lifted from Kubernetes Client - Integer watchReconnectLimit; + int watchReconnectLimit; /** * Maximum amount of time to wait for a connection with the API server to be established @@ -148,5 +147,5 @@ public class KubernetesClientBuildConfig { * IP addresses or hosts to exclude from proxying */ @ConfigItem - List> noProxy; + Optional noProxy; } diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientProducer.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientProducer.java index 5ecb3798fc0fb..59355f75001d6 100644 --- a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientProducer.java +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesClientProducer.java @@ -43,19 +43,11 @@ public Config config() { .withHttpsProxy(buildConfig.httpsProxy.orElse(base.getHttpsProxy())) .withProxyUsername(buildConfig.proxyUsername.orElse(base.getProxyUsername())) .withProxyPassword(buildConfig.proxyPassword.orElse(base.getProxyPassword())) - .withNoProxy(buildConfig.noProxy.size() > 0 ? buildConfig.noProxy.toArray(new String[0]) : base.getNoProxy()) + .withNoProxy(buildConfig.noProxy.isPresent() ? buildConfig.noProxy.get() : base.getNoProxy()) .build(); } - private String or(String value, String fallback) { - if (value.isEmpty()) { - return fallback; - } else { - return value; - } - } - @DefaultBean @Singleton @Produces diff --git a/extensions/kubernetes/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java b/extensions/kubernetes/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java index e48e55286b31c..dc384c358a096 100644 --- a/extensions/kubernetes/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java +++ b/extensions/kubernetes/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java @@ -43,6 +43,7 @@ import io.quarkus.deployment.builditem.ArchiveRootBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedFileSystemResourceBuildItem; +import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthReadinessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesPortBuildItem; @@ -51,13 +52,17 @@ class KubernetesProcessor { private static final String PROPERTY_PREFIX = "dekorate."; - private static final String ALLOWED_GENERATOR = "kubernetes"; + private static final Set ALLOWED_GENERATORS = new HashSet( + Arrays.asList("kubernetes", "openshift", "knative", "docker", "s2i")); + private static final Set IMAGE_GENERATORS = new HashSet(Arrays.asList("docker", "s2i")); private static final String DEPLOYMENT_TARGET = "kubernetes.deployment.target"; private static final String KUBERNETES = "kubernetes"; private static final String DOCKER_REGISTRY_PROPERTY = PROPERTY_PREFIX + "docker.registry"; private static final String APP_GROUP_PROPERTY = "app.group"; + private static final String OUTPUT_ARTIFACT_FORMAT = "%s-%s%s.jar"; + @Inject BuildProducer generatedResourceProducer; @@ -67,6 +72,7 @@ class KubernetesProcessor { @BuildStep(onlyIf = IsNormal.class) public void build(ApplicationInfoBuildItem applicationInfo, ArchiveRootBuildItem archiveRootBuildItem, + PackageConfig packageConfig, List kubernetesRoleBuildItems, List kubernetesPortBuildItems, Optional kubernetesHealthLivenessPathBuildItem, @@ -97,23 +103,32 @@ public void build(ApplicationInfoBuildItem applicationInfo, .collect(Collectors.toList()); Map configAsMap = StreamSupport.stream(config.getPropertyNames().spliterator(), false) - .filter(k -> ALLOWED_GENERATOR.equals(generatorName(k))) + .filter(k -> ALLOWED_GENERATORS.contains(generatorName(k))) .collect(Collectors.toMap(k -> PROPERTY_PREFIX + k, k -> config.getValue(k, String.class))); + // this is a hack to get kubernetes.registry working because currently it's not supported as is in Dekorate - Optional kubernetesRegistry = config.getOptionalValue(ALLOWED_GENERATOR + ".registry", String.class); - if (kubernetesRegistry.isPresent()) { - System.setProperty(DOCKER_REGISTRY_PROPERTY, kubernetesRegistry.get()); - } - // this is a hack to work around Dekorate using the default group for some of the properties - Optional kubernetesGroup = config.getOptionalValue(ALLOWED_GENERATOR + ".group", String.class); - if (kubernetesGroup.isPresent()) { - System.setProperty(APP_GROUP_PROPERTY, kubernetesGroup.get()); - } + Optional dockerRegistry = IMAGE_GENERATORS.stream() + .map(g -> config.getOptionalValue(g + ".registry", String.class)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + + dockerRegistry.ifPresent(v -> System.setProperty(DOCKER_REGISTRY_PROPERTY, v)); + // this is a hack to work around Dekorate using the default group for some of the properties + Optional kubernetesGroup = ALLOWED_GENERATORS.stream() + .map(g -> config.getOptionalValue(g + ".group", String.class)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + kubernetesGroup.ifPresent(v -> System.setProperty(APP_GROUP_PROPERTY, v)); + + Path artifactPath = archiveRootBuildItem.getArchiveRoot().resolve(String.format(OUTPUT_ARTIFACT_FORMAT, + applicationInfo.getName(), applicationInfo.getVersion(), packageConfig.runnerSuffix)); final Map generatedResourcesMap; try { final SessionWriter sessionWriter = new SimpleFileWriter(root, false); - Project project = createProject(applicationInfo, archiveRootBuildItem); + Project project = createProject(applicationInfo, artifactPath); sessionWriter.setProject(project); final Session session = Session.getSession(); session.setWriter(sessionWriter); @@ -131,7 +146,7 @@ public void build(ApplicationInfoBuildItem applicationInfo, if (kubernetesGroup.isPresent()) { System.clearProperty(APP_GROUP_PROPERTY); } - if (kubernetesRegistry.isPresent()) { + if (dockerRegistry.isPresent()) { System.clearProperty(DOCKER_REGISTRY_PROPERTY); } } @@ -215,11 +230,12 @@ private Map verifyPorts(List kubernete return result; } - private Project createProject(ApplicationInfoBuildItem app, ArchiveRootBuildItem archiveRootBuildItem) { + private Project createProject(ApplicationInfoBuildItem app, Path artifactPath) { //Let dekorate create a Project instance and then override with what is found in ApplicationInfoBuildItem. - Project project = FileProjectFactory.create(archiveRootBuildItem.getArchiveLocation().toFile()); + Project project = FileProjectFactory.create(artifactPath.toFile()); BuildInfo buildInfo = new BuildInfo(app.getName(), app.getVersion(), "jar", project.getBuildInfo().getBuildTool(), + artifactPath, project.getBuildInfo().getOutputFile(), project.getBuildInfo().getClassOutputDir()); diff --git a/extensions/logging-gelf/deployment/pom.xml b/extensions/logging-gelf/deployment/pom.xml new file mode 100644 index 0000000000000..193d448ab45db --- /dev/null +++ b/extensions/logging-gelf/deployment/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + io.quarkus + quarkus-logging-gelf-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-gelf-deployment + Quarkus - Logging - GELF - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-logging-gelf + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/logging-gelf/deployment/src/main/java/io/quarkus/logging/gelf/deployment/GelfLogHandlerProcessor.java b/extensions/logging-gelf/deployment/src/main/java/io/quarkus/logging/gelf/deployment/GelfLogHandlerProcessor.java new file mode 100644 index 0000000000000..7b3aae12fa0a7 --- /dev/null +++ b/extensions/logging-gelf/deployment/src/main/java/io/quarkus/logging/gelf/deployment/GelfLogHandlerProcessor.java @@ -0,0 +1,58 @@ +package io.quarkus.logging.gelf.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.logging.gelf.GelfConfig; +import io.quarkus.logging.gelf.GelfLogHandlerRecorder; + +class GelfLogHandlerProcessor { + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FeatureBuildItem.LOGGING_GELF); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + LogHandlerBuildItem build(GelfLogHandlerRecorder recorder, GelfConfig config) { + return new LogHandlerBuildItem(recorder.initializeHandler(config)); + } + + @BuildStep + RuntimeInitializedClassBuildItem nativeBuild() { + return new RuntimeInitializedClassBuildItem( + "biz.paluch.logging.gelf.jboss7.JBoss7GelfLogHandler"); + } + + @BuildStep() + SystemPropertyBuildItem sysProp() { + //FIXME we change the order ot the Hostname resolution for native image + // see https://logging.paluch.biz/hostname-lookup.html + // if not, we have the following error + /* + * java.lang.NullPointerException + * at java.net.InetAddress.getHostFromNameService(InetAddress.java:615) + * at java.net.InetAddress.getCanonicalHostName(InetAddress.java:589) + * at biz.paluch.logging.RuntimeContainer.isQualified(RuntimeContainer.java:20) + * at biz.paluch.logging.RuntimeContainer.getInetAddressWithHostname(RuntimeContainer.java:137) + * at biz.paluch.logging.RuntimeContainer.lookupHostname(RuntimeContainer.java:90) + * at biz.paluch.logging.RuntimeContainer.initialize(RuntimeContainer.java:67) + * at biz.paluch.logging.gelf.jul.GelfLogHandler.(GelfLogHandler.java:54) + * at biz.paluch.logging.gelf.jboss7.JBoss7GelfLogHandler.(JBoss7GelfLogHandler.java:75) + * at io.quarkus.logging.gelf.GelfLogHandlerRecorder.initializeHandler(GelfLogHandlerRecorder.java:17) + * at io.quarkus.deployment.steps.GelfLogHandlerProcessor$build27.deploy_0(GelfLogHandlerProcessor$build27.zig:76) + * at io.quarkus.deployment.steps.GelfLogHandlerProcessor$build27.deploy(GelfLogHandlerProcessor$build27.zig:36) + * at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:68) + * at io.quarkus.runtime.Application.start(Application.java:87) + * at io.quarkus.runtime.Application.run(Application.java:210) + * at io.quarkus.runner.GeneratedMain.main(GeneratedMain.zig:41) + */ + return new SystemPropertyBuildItem("logstash-gelf.resolutionOrder", "localhost,network"); + } + +} diff --git a/extensions/logging-gelf/pom.xml b/extensions/logging-gelf/pom.xml new file mode 100644 index 0000000000000..b25cb1a3ba762 --- /dev/null +++ b/extensions/logging-gelf/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + io.quarkus + quarkus-extensions-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-gelf-parent + Quarkus - Logging - GELF + + pom + + deployment + runtime + + diff --git a/extensions/logging-gelf/runtime/pom.xml b/extensions/logging-gelf/runtime/pom.xml new file mode 100644 index 0000000000000..0e3be8d0bdae6 --- /dev/null +++ b/extensions/logging-gelf/runtime/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + io.quarkus + quarkus-logging-gelf-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-gelf + Quarkus - Logging - GELF - Runtime + Log using the Graylog Extended Log Format and centralize your logs in ELK or EFK + + + + biz.paluch.logging + logstash-gelf + + + com.oracle.substratevm + svm + + + io.quarkus + quarkus-core + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/AdditionalFieldConfig.java b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/AdditionalFieldConfig.java new file mode 100644 index 0000000000000..70a852bcba223 --- /dev/null +++ b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/AdditionalFieldConfig.java @@ -0,0 +1,24 @@ +package io.quarkus.logging.gelf; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Post additional fields. E.g. `fieldName1=value1,fieldName2=value2`. + */ +@ConfigGroup +public class AdditionalFieldConfig { + /** + * Additional field value. + */ + @ConfigItem + public String value; + + /** + * Additional field type specification. + * Supported types: String, long, Long, double, Double and discover. + * Discover is the default if not specified, it discovers field type based on parseability. + */ + @ConfigItem(defaultValue = "discover") + public String type; +} diff --git a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfConfig.java b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfConfig.java new file mode 100644 index 0000000000000..906663f733ff2 --- /dev/null +++ b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfConfig.java @@ -0,0 +1,101 @@ +package io.quarkus.logging.gelf; + +import java.util.Map; +import java.util.logging.Level; + +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "log.handler.gelf") +public class GelfConfig { + /** + * Determine whether to enable the GELF logging handler + */ + @ConfigItem + public boolean enabled; + + /** + * Hostname/IP-Address of the Logstash/Graylog Host + * By default it uses UDP, prepend tcp: to the hostname to switch to TCP, example: "tcp:localhost" + */ + @ConfigItem(defaultValue = "localhost") + public String host; + + /** + * The port + */ + @ConfigItem(defaultValue = "12201") + public int port; + + /** + * GELF version: 1.0 or 1.1 + */ + @ConfigItem(defaultValue = "1.1") + public String version; + + /** + * Whether to post Stack-Trace to StackTrace field. + * + * @see #stackTraceThrowableReference to customize the way the Stack-Trace is handled. + */ + @ConfigItem(defaultValue = "true") + public boolean extractStackTrace; + + /** + * Only used when `extractStackTrace` is `true`. + * A value of 0 will extract the whole stack trace. + * Any positive value will walk the cause chain: 1 corresponds with exception.getCause(), + * 2 with exception.getCause().getCause(), ... + * Negative throwable reference walk the exception chain from the root cause side: -1 will extract the root cause, + * -2 the exception wrapping the root cause, ... + */ + @ConfigItem(defaultValue = "0") + public int stackTraceThrowableReference; + + /** + * Whether to perform Stack-Trace filtering + */ + @ConfigItem + public boolean filterStackTrace; + + /** + * Java date pattern, see {@link java.text.SimpleDateFormat} + */ + @ConfigItem(defaultValue = "yyyy-MM-dd HH:mm:ss,SSS") + public String timestampPattern; + + /** + * The logging-gelf log level. + */ + @ConfigItem(defaultValue = "ALL") + public Level level; + + /** + * Name of the facility. + */ + @ConfigItem(defaultValue = "jboss-logmanager") + public String facility; + + /** + * Post additional fields. + * You can add static fields to each log event in the following form: + * + *

+     * quarkus.log.handler.gelf.additional-field.field1.value=value1
+     * quarkus.log.handler.gelf.additional-field.field1.type=String
+     * 
+ */ + @ConfigItem + @ConfigDocMapKey("field-name") + @ConfigDocSection + public Map additionalField; + + /** + * Whether to include all fields from the MDC. + */ + @ConfigItem(defaultValue = "false") + public boolean includeFullMdc; +} diff --git a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfLogHandlerRecorder.java b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfLogHandlerRecorder.java new file mode 100644 index 0000000000000..0c845cb03eb05 --- /dev/null +++ b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfLogHandlerRecorder.java @@ -0,0 +1,55 @@ +package io.quarkus.logging.gelf; + +import java.util.Map; +import java.util.Optional; +import java.util.logging.Handler; + +import biz.paluch.logging.gelf.jboss7.JBoss7GelfLogHandler; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class GelfLogHandlerRecorder { + public RuntimeValue> initializeHandler(final GelfConfig config) { + if (!config.enabled) { + return new RuntimeValue<>(Optional.empty()); + } + + final JBoss7GelfLogHandler handler = new JBoss7GelfLogHandler(); + handler.setVersion(config.version); + handler.setFacility(config.facility); + String extractStackTrace = String.valueOf(config.extractStackTrace); + if (config.extractStackTrace && config.stackTraceThrowableReference != 0) { + extractStackTrace = String.valueOf(config.stackTraceThrowableReference); + } + handler.setExtractStackTrace(extractStackTrace); + handler.setFilterStackTrace(config.filterStackTrace); + handler.setTimestampPattern(config.timestampPattern); + handler.setIncludeFullMdc(config.includeFullMdc); + handler.setHost(config.host); + handler.setPort(config.port); + handler.setLevel(config.level); + + // handle additional fields + if (!config.additionalField.isEmpty()) { + StringBuilder additionalFieldsValue = new StringBuilder(); + StringBuilder additionalFieldsType = new StringBuilder(); + for (Map.Entry additionalField : config.additionalField.entrySet()) { + if (additionalFieldsValue.length() > 0) { + additionalFieldsValue.append(','); + } + additionalFieldsValue.append(additionalField.getKey()).append('=').append(additionalField.getValue().value); + + if (additionalFieldsType.length() > 0) { + additionalFieldsType.append(','); + } + additionalFieldsType.append(additionalField.getKey()).append('=').append(additionalField.getValue().type); + } + + handler.setAdditionalFields(additionalFieldsValue.toString()); + handler.setAdditionalFieldTypes(additionalFieldsType.toString()); + } + + return new RuntimeValue<>(Optional.of(handler)); + } +} diff --git a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/graal/KafkaGelfSenderProviderSubstitution.java b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/graal/KafkaGelfSenderProviderSubstitution.java new file mode 100644 index 0000000000000..ad0f94e6ee8e1 --- /dev/null +++ b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/graal/KafkaGelfSenderProviderSubstitution.java @@ -0,0 +1,17 @@ +package io.quarkus.logging.gelf.graal; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import biz.paluch.logging.gelf.intern.GelfSender; +import biz.paluch.logging.gelf.intern.GelfSenderConfiguration; + +@TargetClass(className = "biz.paluch.logging.gelf.intern.sender.KafkaGelfSenderProvider") +public final class KafkaGelfSenderProviderSubstitution { + + @Substitute + public GelfSender create(GelfSenderConfiguration configuration) { + return null; + } + +} diff --git a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/graal/RedisGelfSenderProviderSubstitution.java b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/graal/RedisGelfSenderProviderSubstitution.java new file mode 100644 index 0000000000000..be9cbd5d7cbd9 --- /dev/null +++ b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/graal/RedisGelfSenderProviderSubstitution.java @@ -0,0 +1,15 @@ +package io.quarkus.logging.gelf.graal; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import biz.paluch.logging.gelf.intern.GelfSender; +import biz.paluch.logging.gelf.intern.GelfSenderConfiguration; + +@TargetClass(className = "biz.paluch.logging.gelf.intern.sender.RedisGelfSenderProvider") +public final class RedisGelfSenderProviderSubstitution { + @Substitute + public GelfSender create(GelfSenderConfiguration configuration) { + return null; + } +} diff --git a/extensions/logging-gelf/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/logging-gelf/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..36df0f176b177 --- /dev/null +++ b/extensions/logging-gelf/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +name: "Logging GELF" +metadata: + keywords: + - "logging" + - "gelf" + - "handler" + guide: "https://quarkus.io/guides/centralized-log-management" + categories: + - "core" + status: "preview" diff --git a/extensions/logging-json/deployment/pom.xml b/extensions/logging-json/deployment/pom.xml new file mode 100644 index 0000000000000..dcf12462ed46d --- /dev/null +++ b/extensions/logging-json/deployment/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + + io.quarkus + quarkus-logging-json-parent + 999-SNAPSHOT + ../ + + + quarkus-logging-json-deployment + Quarkus - Logging - JSON - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-logging-json + + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkus + quarkus-junit5 + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-arc-deployment + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + + \ No newline at end of file diff --git a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonSteps.java b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonSteps.java new file mode 100644 index 0000000000000..f09287aae3204 --- /dev/null +++ b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonSteps.java @@ -0,0 +1,17 @@ +package io.quarkus.logging.json.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; +import io.quarkus.logging.json.runtime.JsonConfig; +import io.quarkus.logging.json.runtime.LoggingJsonRecorder; + +public final class LoggingJsonSteps { + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public LogConsoleFormatBuildItem setUpFormatter(LoggingJsonRecorder recorder, JsonConfig config) { + return new LogConsoleFormatBuildItem(recorder.initializeJsonLogging(config)); + } +} diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterCustomConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterCustomConfigTest.java new file mode 100644 index 0000000000000..d99145d123d68 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterCustomConfigTest.java @@ -0,0 +1,33 @@ +package io.quarkus.logging.json; + +import static io.quarkus.logging.json.JsonFormatterDefaultConfigTest.getJsonFormatter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; + +import org.jboss.logmanager.formatters.JsonFormatter; +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class JsonFormatterCustomConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-json-formatter-custom.properties"); + + @Test + public void jsonFormatterCustomConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isTrue(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo("Value(DayOfMonth)' 'Text(MonthOfYear,SHORT)' 'Value(Year,4,19,EXCEEDS_PAD)"); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.of("UTC+05:00")); + assertThat(jsonFormatter.getExceptionOutputType()) + .isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED_AND_FORMATTED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n;"); + assertThat(jsonFormatter.isPrintDetails()).isTrue(); + } +} diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterDefaultConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterDefaultConfigTest.java new file mode 100644 index 0000000000000..9fa56df1e4cbd --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterDefaultConfigTest.java @@ -0,0 +1,60 @@ +package io.quarkus.logging.json; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import org.jboss.logmanager.formatters.JsonFormatter; +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.jboss.logmanager.handlers.ConsoleHandler; +import org.jboss.logmanager.handlers.DelayedHandler; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.logging.InitialConfigurator; +import io.quarkus.test.QuarkusUnitTest; + +public class JsonFormatterDefaultConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-json-formatter-default.properties"); + + @Test + public void jsonFormatterDefaultConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isFalse(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()).toString()); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.systemDefault()); + assertThat(jsonFormatter.getExceptionOutputType()).isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n"); + assertThat(jsonFormatter.isPrintDetails()).isFalse(); + } + + public static JsonFormatter getJsonFormatter() { + LogManager logManager = LogManager.getLogManager(); + assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); + + DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; + assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); + assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); + + Handler handler = Arrays.stream(delayedHandler.getHandlers()) + .filter(h -> (h instanceof ConsoleHandler)) + .findFirst().orElse(null); + assertThat(handler).isNotNull(); + assertThat(handler.getLevel()).isEqualTo(Level.WARNING); + + Formatter formatter = handler.getFormatter(); + assertThat(formatter).isInstanceOf(JsonFormatter.class); + return (JsonFormatter) formatter; + } +} diff --git a/extensions/logging-json/deployment/src/test/resources/application-json-formatter-custom.properties b/extensions/logging-json/deployment/src/test/resources/application-json-formatter-custom.properties new file mode 100644 index 0000000000000..485f7fbc6c62f --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-json-formatter-custom.properties @@ -0,0 +1,11 @@ +quarkus.log.level=INFO +quarkus.log.console.enable=true +quarkus.log.console.level=WARNING +quarkus.log.console.json=true +quarkus.log.console.json.pretty-print=true +quarkus.log.console.json.date-format=d MMM uuuu +quarkus.log.console.json.record-delimiter=\n; +quarkus.log.console.json.zone-id=UTC+05:00 +quarkus.log.console.json.exception-output-type=DETAILED_AND_FORMATTED +quarkus.log.console.json.print-details=true + diff --git a/extensions/logging-json/deployment/src/test/resources/application-json-formatter-default.properties b/extensions/logging-json/deployment/src/test/resources/application-json-formatter-default.properties new file mode 100644 index 0000000000000..359d81b0e740e --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-json-formatter-default.properties @@ -0,0 +1,5 @@ +quarkus.log.level=INFO +quarkus.log.console.enable=true +quarkus.log.console.level=WARNING +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.console.json=true diff --git a/extensions/logging-json/pom.xml b/extensions/logging-json/pom.xml new file mode 100644 index 0000000000000..063b93e5e5f7a --- /dev/null +++ b/extensions/logging-json/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-logging-json-parent + Quarkus - Logging - JSON + pom + + + deployment + runtime + + + \ No newline at end of file diff --git a/extensions/logging-json/runtime/pom.xml b/extensions/logging-json/runtime/pom.xml new file mode 100644 index 0000000000000..8bb3f44c6e74e --- /dev/null +++ b/extensions/logging-json/runtime/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + io.quarkus + quarkus-logging-json-parent + 999-SNAPSHOT + ../ + + + quarkus-logging-json + + Quarkus - Logging - JSON - Runtime + Add JSON formatter for console logging + + + + io.quarkus + quarkus-core + + + + io.quarkus + quarkus-jsonp + + + + org.jboss.logmanager + jboss-logmanager-embedded + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + \ No newline at end of file diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonConfig.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonConfig.java new file mode 100644 index 0000000000000..31e50df91d66c --- /dev/null +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonConfig.java @@ -0,0 +1,54 @@ +package io.quarkus.logging.json.runtime; + +import java.util.Optional; + +import org.jboss.logmanager.formatters.StructuredFormatter; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Configuration for JSON log formatting. + */ +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "log.console.json") +public class JsonConfig { + /** + * Determine whether to enable the JSON console formatting extension, which disables "normal" console formatting. + */ + @ConfigItem(name = ConfigItem.PARENT, defaultValue = "true") + boolean enable; + /** + * Enable "pretty printing" of the JSON record. Note that some JSON parsers will fail to read pretty printed output. + */ + @ConfigItem + boolean prettyPrint; + /** + * The date format to use. The special string "default" indicates that the default format should be used. + */ + @ConfigItem(defaultValue = "default") + String dateFormat; + /** + * The special end-of-record delimiter to be used. By default, no delimiter is used. + */ + @ConfigItem + Optional recordDelimiter; + /** + * The zone ID to use. The special string "default" indicates that the default zone should be used. + */ + @ConfigItem(defaultValue = "default") + String zoneId; + /** + * The exception output type to specify. + */ + @ConfigItem(defaultValue = "detailed") + StructuredFormatter.ExceptionOutputType exceptionOutputType; + /** + * Enable printing of more details in the log. + *

+ * Printing the details can be expensive as the values are retrieved from the caller. The details include the + * source class name, source file name, source method name and source line number. + */ + @ConfigItem + boolean printDetails; +} diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java new file mode 100644 index 0000000000000..639f8cb62a3a5 --- /dev/null +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java @@ -0,0 +1,32 @@ +package io.quarkus.logging.json.runtime; + +import java.util.Optional; +import java.util.logging.Formatter; + +import org.jboss.logmanager.formatters.JsonFormatter; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class LoggingJsonRecorder { + public RuntimeValue> initializeJsonLogging(final JsonConfig config) { + if (!config.enable) { + return new RuntimeValue<>(Optional.empty()); + } + final JsonFormatter formatter = new JsonFormatter(); + formatter.setPrettyPrint(config.prettyPrint); + final String dateFormat = config.dateFormat; + if (!dateFormat.equals("default")) { + formatter.setDateFormat(dateFormat); + } + formatter.setExceptionOutputType(config.exceptionOutputType); + formatter.setPrintDetails(config.printDetails); + config.recordDelimiter.ifPresent(formatter::setRecordDelimiter); + final String zoneId = config.zoneId; + if (!zoneId.equals("default")) { + formatter.setZoneId(zoneId); + } + return new RuntimeValue<>(Optional.of(formatter)); + } +} diff --git a/extensions/logging-json/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/logging-json/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..01d0cd843ec6c --- /dev/null +++ b/extensions/logging-json/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +name: "Logging JSON" +metadata: + keywords: + - "logging" + - "json" + - "formatter" + categories: + - "core" + status: "preview" + guide: "https://quarkus.io/guides/logging#json-logging" \ No newline at end of file diff --git a/extensions/logging-sentry/deployment/pom.xml b/extensions/logging-sentry/deployment/pom.xml new file mode 100644 index 0000000000000..df0b5a712d4e0 --- /dev/null +++ b/extensions/logging-sentry/deployment/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + io.quarkus + quarkus-logging-sentry-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-sentry-deployment + Quarkus - Logging - Sentry - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-logging-sentry + + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkus + quarkus-junit5 + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-arc-deployment + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/logging-sentry/deployment/src/main/java/io/quarkus/logging/sentry/deployment/SentryProcessor.java b/extensions/logging-sentry/deployment/src/main/java/io/quarkus/logging/sentry/deployment/SentryProcessor.java new file mode 100644 index 0000000000000..f301eb67b2a86 --- /dev/null +++ b/extensions/logging-sentry/deployment/src/main/java/io/quarkus/logging/sentry/deployment/SentryProcessor.java @@ -0,0 +1,33 @@ +package io.quarkus.logging.sentry.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.logging.sentry.SentryConfig; +import io.quarkus.logging.sentry.SentryHandlerValueFactory; + +class SentryProcessor { + + private static final String FEATURE = "sentry"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + LogHandlerBuildItem addSentryLogHandler(final SentryConfig sentryConfig, + final SentryHandlerValueFactory sentryHandlerValueFactory) { + return new LogHandlerBuildItem(sentryHandlerValueFactory.create(sentryConfig)); + } + + @BuildStep + ExtensionSslNativeSupportBuildItem activateSslNativeSupport() { + return new ExtensionSslNativeSupportBuildItem(FEATURE); + } + +} diff --git a/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerCustomTest.java b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerCustomTest.java new file mode 100644 index 0000000000000..c1864feb9389a --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerCustomTest.java @@ -0,0 +1,33 @@ +package io.quarkus.logging.sentry; + +import static io.quarkus.logging.sentry.SentryLoggerTest.getSentryHandler; +import static io.sentry.jvmti.ResetFrameCache.resetFrameCache; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.sentry.jul.SentryHandler; +import io.sentry.jvmti.FrameCache; + +public class SentryLoggerCustomTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-sentry-logger-custom.properties"); + + @Test + public void sentryLoggerCustomTest() { + final SentryHandler sentryHandler = getSentryHandler(); + assertThat(sentryHandler).isNotNull(); + assertThat(sentryHandler.getLevel()).isEqualTo(org.jboss.logmanager.Level.TRACE); + assertThat(FrameCache.shouldCacheThrowable(new IllegalStateException("Test frame"), 1)).isTrue(); + } + + @AfterAll + static void reset() { + resetFrameCache(); + } +} diff --git a/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerDisabledTest.java b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerDisabledTest.java new file mode 100644 index 0000000000000..1164b779aadd7 --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerDisabledTest.java @@ -0,0 +1,30 @@ +package io.quarkus.logging.sentry; + +import static io.quarkus.logging.sentry.SentryLoggerTest.getSentryHandler; +import static io.sentry.jvmti.ResetFrameCache.resetFrameCache; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.sentry.jul.SentryHandler; + +public class SentryLoggerDisabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-sentry-logger-disabled.properties"); + + @Test + public void sentryLoggerDisabledTest() { + final SentryHandler sentryHandler = getSentryHandler(); + assertThat(sentryHandler).isNull(); + } + + @AfterAll + static void reset() { + resetFrameCache(); + } +} diff --git a/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerTest.java b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerTest.java new file mode 100644 index 0000000000000..37ced7acdd330 --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerTest.java @@ -0,0 +1,54 @@ +package io.quarkus.logging.sentry; + +import static io.sentry.jvmti.ResetFrameCache.resetFrameCache; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import org.jboss.logmanager.handlers.DelayedHandler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.logging.InitialConfigurator; +import io.quarkus.test.QuarkusUnitTest; +import io.sentry.jul.SentryHandler; +import io.sentry.jvmti.FrameCache; + +public class SentryLoggerTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-sentry-logger-default.properties"); + + @Test + public void sentryLoggerDefaultTest() { + final SentryHandler sentryHandler = getSentryHandler(); + assertThat(sentryHandler).isNotNull(); + assertThat(sentryHandler.getLevel()).isEqualTo(org.jboss.logmanager.Level.WARN); + assertThat(FrameCache.shouldCacheThrowable(new IllegalStateException("Test frame"), 1)).isFalse(); + } + + public static SentryHandler getSentryHandler() { + LogManager logManager = LogManager.getLogManager(); + assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); + + DelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; + assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); + assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); + + Handler handler = Arrays.stream(delayedHandler.getHandlers()) + .filter(h -> (h instanceof SentryHandler)) + .findFirst().orElse(null); + return (SentryHandler) handler; + } + + @AfterAll + static void reset() { + resetFrameCache(); + } +} diff --git a/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerWrongTest.java b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerWrongTest.java new file mode 100644 index 0000000000000..7eaca71ae83be --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/java/io/quarkus/logging/sentry/SentryLoggerWrongTest.java @@ -0,0 +1,28 @@ +package io.quarkus.logging.sentry; + +import static io.sentry.jvmti.ResetFrameCache.resetFrameCache; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class SentryLoggerWrongTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-sentry-logger-wrong.properties") + .setExpectedException(ConfigurationException.class); + + @Test + public void sentryLoggerWrongTest() { + //Exception is expected + } + + @AfterAll + static void reset() { + resetFrameCache(); + } +} diff --git a/extensions/logging-sentry/deployment/src/test/java/io/sentry/jvmti/ResetFrameCache.java b/extensions/logging-sentry/deployment/src/test/java/io/sentry/jvmti/ResetFrameCache.java new file mode 100644 index 0000000000000..5b8156ddf6f33 --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/java/io/sentry/jvmti/ResetFrameCache.java @@ -0,0 +1,8 @@ +package io.sentry.jvmti; + +public class ResetFrameCache { + + public static void resetFrameCache() { + FrameCache.reset(); + } +} diff --git a/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-custom.properties b/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-custom.properties new file mode 100644 index 0000000000000..ebe3085388d2b --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-custom.properties @@ -0,0 +1,4 @@ +quarkus.log.sentry=true +quarkus.log.sentry.dsn=https://123@test.io/22222 +quarkus.log.sentry.level=TRACE +quarkus.log.sentry.in-app-packages=io.quarkus.logging.sentry,org.test \ No newline at end of file diff --git a/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-default.properties b/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-default.properties new file mode 100644 index 0000000000000..cf1755dd7a2de --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-default.properties @@ -0,0 +1,3 @@ +quarkus.log.sentry=true +quarkus.log.sentry.dsn=https://123@test.io/22222 +quarkus.log.sentry.in-app-packages=* \ No newline at end of file diff --git a/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-disabled.properties b/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-disabled.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-wrong.properties b/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-wrong.properties new file mode 100644 index 0000000000000..d2c579d84ad2c --- /dev/null +++ b/extensions/logging-sentry/deployment/src/test/resources/application-sentry-logger-wrong.properties @@ -0,0 +1 @@ +quarkus.log.sentry=true \ No newline at end of file diff --git a/extensions/logging-sentry/pom.xml b/extensions/logging-sentry/pom.xml new file mode 100644 index 0000000000000..c73a89369c484 --- /dev/null +++ b/extensions/logging-sentry/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + quarkus-logging-sentry-parent + Quarkus - Logging - Sentry + + pom + + + deployment + runtime + + diff --git a/extensions/logging-sentry/runtime/pom.xml b/extensions/logging-sentry/runtime/pom.xml new file mode 100644 index 0000000000000..5decc83d22da5 --- /dev/null +++ b/extensions/logging-sentry/runtime/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + io.quarkus + quarkus-logging-sentry-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-sentry + Quarkus - Logging - Sentry - Runtime + Use Sentry, a self-hosted or cloud-based error monitoring solution + + + + io.quarkus + quarkus-core + + + io.sentry + sentry + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryConfig.java b/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryConfig.java new file mode 100644 index 0000000000000..3bba67dc35165 --- /dev/null +++ b/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryConfig.java @@ -0,0 +1,50 @@ +package io.quarkus.logging.sentry; + +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Configuration for Sentry logging. + */ +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "log.sentry") +public class SentryConfig { + + /** + * Determine whether to enable the Sentry logging extension. + */ + @ConfigItem(name = ConfigItem.PARENT) + boolean enable; + + /** + * Sentry DSN + * + * The DSN is the first and most important thing to configure because it tells the SDK where to send events. You can find + * your project’s DSN in the “Client Keys” section of your “Project Settings” in Sentry. + */ + @ConfigItem + public Optional dsn; + + /** + * The sentry log level. + */ + @ConfigItem(defaultValue = "WARN") + public Level level; + + /** + * Sentry differentiates stack frames that are directly related to your application (“in application”) from stack frames + * that come from other packages such as the standard library, frameworks, or other dependencies. The difference is visible + * in the Sentry web interface where only the “in application” frames are displayed by default. + * + * You can configure which package prefixes your application uses with this option. + * + * This option is highly recommended as it affects stacktrace grouping and display on Sentry. See documentation: + * https://quarkus.io/guides/logging-sentry#in-app-packages + */ + @ConfigItem + public Optional> inAppPackages; +} diff --git a/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryConfigProvider.java b/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryConfigProvider.java new file mode 100644 index 0000000000000..5a3f8b39c1619 --- /dev/null +++ b/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryConfigProvider.java @@ -0,0 +1,33 @@ +package io.quarkus.logging.sentry; + +import static java.lang.String.join; + +import java.util.Objects; + +import io.sentry.DefaultSentryClientFactory; +import io.sentry.config.provider.ConfigurationProvider; + +/** + * Mapping between the SentryConfig and the Sentry options {@link io.sentry.DefaultSentryClientFactory} + */ +class SentryConfigProvider implements ConfigurationProvider { + + private final SentryConfig config; + + SentryConfigProvider(SentryConfig config) { + this.config = config; + } + + @Override + public String getProperty(String key) { + switch (key) { + case DefaultSentryClientFactory.IN_APP_FRAMES_OPTION: + return config.inAppPackages.map(p -> join(",", p)) + .filter(s -> !Objects.equals(s, "*")) + .orElse(""); + // New SentryConfig options should be mapped here + default: + return null; + } + } +} diff --git a/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryHandlerValueFactory.java b/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryHandlerValueFactory.java new file mode 100644 index 0000000000000..f70ec5dd378dc --- /dev/null +++ b/extensions/logging-sentry/runtime/src/main/java/io/quarkus/logging/sentry/SentryHandlerValueFactory.java @@ -0,0 +1,38 @@ +package io.quarkus.logging.sentry; + +import java.util.Optional; +import java.util.logging.Handler; + +import org.jboss.logging.Logger; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.sentry.Sentry; +import io.sentry.SentryOptions; +import io.sentry.config.Lookup; +import io.sentry.jul.SentryHandler; + +@Recorder +public class SentryHandlerValueFactory { + private static final Logger LOG = Logger.getLogger(SentryConfigProvider.class); + + public RuntimeValue> create(final SentryConfig config) { + if (!config.enable) { + return new RuntimeValue<>(Optional.empty()); + } + if (!config.dsn.isPresent()) { + throw new ConfigurationException( + "Configuration key \"quarkus.log.sentry.dsn\" is required when Sentry is enabled, but its value is empty/missing"); + } + if (!config.inAppPackages.isPresent()) { + LOG.warn( + "No 'quarkus.sentry.in-app-packages' was configured, this option is highly recommended as it affects stacktrace grouping and display on Sentry. See https://quarkus.io/guides/logging-sentry#in-app-packages"); + } + final SentryConfigProvider provider = new SentryConfigProvider(config); + Sentry.init(SentryOptions.from(new Lookup(provider, provider), config.dsn.get())); + SentryHandler handler = new SentryHandler(); + handler.setLevel(config.level); + return new RuntimeValue<>(Optional.of(handler)); + } +} diff --git a/extensions/logging-sentry/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/logging-sentry/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..a33c51adf5586 --- /dev/null +++ b/extensions/logging-sentry/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +name: "Logging Sentry" +metadata: + keywords: + - "logging" + - "sentry" + - "cloud" + categories: + - "core" + status: "preview" + guide: "https://quarkus.io/guides/logging-sentry" \ No newline at end of file diff --git a/extensions/mailer/deployment/pom.xml b/extensions/mailer/deployment/pom.xml index e96876f17fd84..aec7797c61584 100644 --- a/extensions/mailer/deployment/pom.xml +++ b/extensions/mailer/deployment/pom.xml @@ -20,6 +20,10 @@ io.quarkus quarkus-vertx-deployment + + io.quarkus + quarkus-qute-deployment + io.quarkus quarkus-mailer diff --git a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java index 11e508dc0bcfd..1ec90f0f3fd3e 100644 --- a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java +++ b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java @@ -1,7 +1,19 @@ package io.quarkus.mailer.deployment; +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; + import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.arc.processor.BuildExtension; +import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -11,18 +23,24 @@ import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.mailer.MailTemplate; import io.quarkus.mailer.runtime.BlockingMailerImpl; import io.quarkus.mailer.runtime.MailClientProducer; import io.quarkus.mailer.runtime.MailConfig; import io.quarkus.mailer.runtime.MailConfigRecorder; +import io.quarkus.mailer.runtime.MailTemplateProducer; import io.quarkus.mailer.runtime.MockMailboxImpl; import io.quarkus.mailer.runtime.ReactiveMailerImpl; +import io.quarkus.qute.deployment.QuteProcessor; +import io.quarkus.qute.deployment.TemplatePathBuildItem; import io.quarkus.runtime.RuntimeValue; import io.quarkus.vertx.deployment.VertxBuildItem; import io.vertx.ext.mail.MailClient; public class MailerProcessor { + private static final DotName MAIL_TEMPLATE = DotName.createSimple(MailTemplate.class.getName()); + @BuildStep AdditionalBeanBuildItem registerClients() { return AdditionalBeanBuildItem.unremovableOf(MailClientProducer.class); @@ -31,7 +49,8 @@ AdditionalBeanBuildItem registerClients() { @BuildStep AdditionalBeanBuildItem registerMailers() { return AdditionalBeanBuildItem.builder() - .addBeanClasses(ReactiveMailerImpl.class, BlockingMailerImpl.class, MockMailboxImpl.class) + .addBeanClasses(ReactiveMailerImpl.class, BlockingMailerImpl.class, MockMailboxImpl.class, + MailTemplateProducer.class) .build(); } @@ -68,4 +87,50 @@ MailerBuildItem build(BuildProducer feature, MailConfigRecorde return new MailerBuildItem(client); } + + @BuildStep + void validateMailTemplates( + List templatePaths, ValidationPhaseBuildItem validationPhase, + BuildProducer validationErrors) { + + Set filePaths = new HashSet(); + for (TemplatePathBuildItem templatePath : templatePaths) { + String filePath = templatePath.getPath(); + if (File.separatorChar != '/') { + filePath = filePath.replace(File.separatorChar, '/'); + } + if (filePath.endsWith("html") || filePath.endsWith("htm") || filePath.endsWith("txt")) { + // For e-mails we only consider html and txt templates + filePaths.add(filePath); + int idx = filePath.lastIndexOf('.'); + if (idx != -1) { + // Also add version without suffix from the path + // For example for "items.html" also add "items" + filePaths.add(filePath.substring(0, idx)); + } + } + } + + for (InjectionPointInfo injectionPoint : validationPhase.getContext().get(BuildExtension.Key.INJECTION_POINTS)) { + if (injectionPoint.getRequiredType().name().equals(MAIL_TEMPLATE)) { + AnnotationInstance resourcePath = injectionPoint.getRequiredQualifier(QuteProcessor.RESOURCE_PATH); + String name; + if (resourcePath != null) { + name = resourcePath.value().asString(); + } else if (injectionPoint.hasDefaultedQualifier()) { + name = QuteProcessor.getName(injectionPoint); + } else { + name = null; + } + if (name != null) { + // For "@Inject MailTemplate items" we try to match "items" + // For "@ResourcePath("github/pulls") MailTemplate pulls" we try to match "github/pulls" + if (filePaths.stream().noneMatch(path -> path.endsWith(name))) { + validationErrors.produce(new ValidationErrorBuildItem( + new IllegalStateException("No mail template found for " + injectionPoint.getTargetInfo()))); + } + } + } + } + } } diff --git a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java index 28f603c3d0e9b..129291056155f 100644 --- a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java +++ b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java @@ -4,13 +4,16 @@ import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; +import javax.inject.Singleton; import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.qute.api.ResourcePath; import io.quarkus.test.QuarkusUnitTest; import io.vertx.ext.mail.MailClient; @@ -23,7 +26,14 @@ public class InjectionTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(BeanUsingAxleMailClient.class, BeanUsingBareMailClient.class, BeanUsingRxClient.class) .addClasses(BeanUsingBlockingMailer.class, BeanUsingReactiveMailer.class) - .addAsResource("mock-config.properties", "application.properties")); + .addClasses(MailTemplates.class) + .addAsResource("mock-config.properties", "application.properties") + .addAsResource(new StringAsset("" + + "{name}"), "templates/test1.html") + .addAsResource(new StringAsset("" + + "{name}"), "templates/test1.txt") + .addAsResource(new StringAsset("" + + "{name}"), "templates/mails/test2.html")); @Inject BeanUsingAxleMailClient beanUsingBare; @@ -40,13 +50,18 @@ public class InjectionTest { @Inject BeanUsingBlockingMailer beanUsingBlockingMailer; + @Inject + MailTemplates templates; + @Test public void testInjection() { beanUsingAxle.verify(); beanUsingBare.verify(); beanUsingRx.verify(); beanUsingBlockingMailer.verify(); - beanUsingReactiveMailer.verify().toCompletableFuture().join(); + beanUsingReactiveMailer.verify(); + templates.send1(); + templates.send2().toCompletableFuture().join(); } @ApplicationScoped @@ -103,4 +118,23 @@ void verify() { mailer.send(Mail.withText("quarkus@quarkus.io", "test mailer", "blocking test!")); } } + + @Singleton + static class MailTemplates { + + @Inject + MailTemplate test1; + + @ResourcePath("mails/test2") + MailTemplate testMail; + + CompletionStage send1() { + return test1.to("quarkus@quarkus.io").subject("Test").data("name", "John").send(); + } + + CompletionStage send2() { + return testMail.to("quarkus@quarkus.io").subject("Test").data("name", "Lu").send(); + } + + } } diff --git a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/MailTemplateValidationTest.java b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/MailTemplateValidationTest.java new file mode 100644 index 0000000000000..4d58136a273d3 --- /dev/null +++ b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/MailTemplateValidationTest.java @@ -0,0 +1,46 @@ +package io.quarkus.mailer; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.inject.spi.DeploymentException; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MailTemplateValidationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MailTemplates.class) + .addAsResource("mock-config.properties", "application.properties") + .addAsResource(new StringAsset("" + + "{name}"), "templates/test1.html")) + .setExpectedException(DeploymentException.class); + + @Test + public void testValidationFailed() { + // This method should not be invoked + Assertions.fail(); + } + + @Singleton + static class MailTemplates { + + @Inject + MailTemplate doesNotExist; + + CompletionStage send() { + return doesNotExist.to("quarkus@quarkus.io").subject("Test").data("name", "Foo").send(); + } + + } +} diff --git a/extensions/mailer/runtime/pom.xml b/extensions/mailer/runtime/pom.xml index d156dddc2210c..b771259686b78 100644 --- a/extensions/mailer/runtime/pom.xml +++ b/extensions/mailer/runtime/pom.xml @@ -20,11 +20,14 @@ io.quarkus quarkus-vertx + + io.quarkus + quarkus-qute + io.smallrye.reactive smallrye-axle-mail-client - org.subethamail subethasmtp diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/MailTemplate.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/MailTemplate.java new file mode 100644 index 0000000000000..a849b7c151872 --- /dev/null +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/MailTemplate.java @@ -0,0 +1,57 @@ +package io.quarkus.mailer; + +import java.util.concurrent.CompletionStage; + +/** + * Represents an e-mail definition based on a template. + */ +public interface MailTemplate { + + /** + * + * @return a new template instance + */ + MailTemplateInstance instance(); + + default MailTemplateInstance of(Mail mail) { + return instance().mail(mail); + } + + default MailTemplateInstance to(String... values) { + return instance().to(values); + } + + default MailTemplateInstance data(String key, Object value) { + return instance().data(key, value); + } + + /** + * Represents an instance of {@link MailTemplate}. + *

+ * This construct is not thread-safe. + */ + interface MailTemplateInstance { + + MailTemplateInstance mail(Mail mail); + + MailTemplateInstance to(String... to); + + MailTemplateInstance cc(String... cc); + + MailTemplateInstance bcc(String... bcc); + + MailTemplateInstance subject(String subject); + + MailTemplateInstance from(String from); + + MailTemplateInstance replyTo(String replyTo); + + MailTemplateInstance bounceAddress(String bounceAddress); + + MailTemplateInstance data(String key, Object value); + + CompletionStage send(); + + } + +} diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateInstanceImpl.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateInstanceImpl.java new file mode 100644 index 0000000000000..f922231a90950 --- /dev/null +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateInstanceImpl.java @@ -0,0 +1,160 @@ +package io.quarkus.mailer.runtime; + +import static io.quarkus.qute.api.VariantTemplate.SELECTED_VARIANT; +import static io.quarkus.qute.api.VariantTemplate.VARIANTS; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; + +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.MailTemplate; +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.mailer.ReactiveMailer; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Variant; +import io.quarkus.qute.api.VariantTemplate; + +class MailTemplateInstanceImpl implements MailTemplate.MailTemplateInstance { + + private final ReactiveMailer mailer; + private final TemplateInstance templateInstance; + private final Map data; + private Mail mail; + + MailTemplateInstanceImpl(ReactiveMailer mailer, TemplateInstance templateInstance) { + this.mailer = mailer; + this.templateInstance = templateInstance; + this.data = new HashMap<>(); + this.mail = new Mail(); + } + + @Override + public MailTemplateInstance mail(Mail mail) { + this.mail = mail; + return this; + } + + @Override + public MailTemplateInstance to(String... to) { + this.mail.addTo(to); + return this; + } + + @Override + public MailTemplateInstance cc(String... cc) { + this.mail.addCc(cc); + return this; + } + + @Override + public MailTemplateInstance bcc(String... bcc) { + this.mail.addBcc(bcc); + return this; + } + + @Override + public MailTemplateInstance subject(String subject) { + this.mail.setSubject(subject); + return this; + } + + @Override + public MailTemplateInstance from(String from) { + this.mail.setFrom(from); + return this; + } + + @Override + public MailTemplateInstance replyTo(String replyTo) { + this.mail.setReplyTo(replyTo); + return this; + } + + @Override + public MailTemplateInstance bounceAddress(String bounceAddress) { + this.mail.setBounceAddress(bounceAddress); + return this; + } + + @Override + public MailTemplateInstance data(String key, Object value) { + this.data.put(key, value); + return this; + } + + @Override + public CompletionStage send() { + + CompletableFuture result = new CompletableFuture<>(); + + if (templateInstance.getAttribute(VariantTemplate.VARIANTS) != null) { + + List results = new ArrayList<>(); + + @SuppressWarnings("unchecked") + List variants = (List) templateInstance.getAttribute(VARIANTS); + for (Variant variant : variants) { + if (variant.mediaType.equals(Variant.TEXT_HTML) || variant.mediaType.equals(Variant.TEXT_PLAIN)) { + results.add(new Result(variant, + templateInstance.setAttribute(SELECTED_VARIANT, variant).data(data).renderAsync() + .toCompletableFuture())); + } + } + + if (results.isEmpty()) { + throw new IllegalStateException("No suitable template variant found"); + } + + CompletableFuture all = CompletableFuture + .allOf(results.stream().map(Result::getValue).toArray(CompletableFuture[]::new)); + all.whenComplete((r1, t1) -> { + if (t1 != null) { + result.completeExceptionally(t1); + } else { + try { + for (Result res : results) { + if (res.variant.mediaType.equals(Variant.TEXT_HTML)) { + mail.setHtml(res.value.get()); + } else if (res.variant.mediaType.equals(Variant.TEXT_PLAIN)) { + mail.setText(res.value.get()); + } + } + } catch (InterruptedException | ExecutionException e) { + result.completeExceptionally(e); + } + mailer.send(mail).whenComplete((r, t) -> { + if (t != null) { + result.completeExceptionally(t); + } else { + result.complete(null); + } + }); + } + }); + } else { + throw new IllegalStateException("No template variant found"); + } + return result; + } + + static class Result { + + final Variant variant; + final CompletableFuture value; + + public Result(Variant variant, CompletableFuture result) { + this.variant = variant; + this.value = result; + } + + CompletableFuture getValue() { + return value; + } + } + +} diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateProducer.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateProducer.java new file mode 100644 index 0000000000000..1e02de0be0619 --- /dev/null +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailTemplateProducer.java @@ -0,0 +1,79 @@ +package io.quarkus.mailer.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import javax.enterprise.inject.Any; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.AnnotatedParameter; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.logging.Logger; + +import io.quarkus.mailer.MailTemplate; +import io.quarkus.mailer.ReactiveMailer; +import io.quarkus.qute.api.ResourcePath; +import io.quarkus.qute.api.VariantTemplate; + +@Singleton +public class MailTemplateProducer { + + private static final Logger LOGGER = Logger.getLogger(MailTemplateProducer.class); + + @Inject + ReactiveMailer mailer; + + @Any + Instance template; + + @Produces + MailTemplate getDefault(InjectionPoint injectionPoint) { + + final String name; + if (injectionPoint.getMember() instanceof Field) { + // For "@Inject MailTemplate test" use "test" + name = injectionPoint.getMember().getName(); + } else { + AnnotatedParameter parameter = (AnnotatedParameter) injectionPoint.getAnnotated(); + if (parameter.getJavaParameter().isNamePresent()) { + name = parameter.getJavaParameter().getName(); + } else { + name = injectionPoint.getMember().getName(); + LOGGER.warnf("Parameter name not present - using the method name as the template name instead %s", name); + } + } + + return new MailTemplate() { + @Override + public MailTemplateInstance instance() { + return new MailTemplateInstanceImpl(mailer, template.select(new ResourcePath.Literal(name)).get().instance()); + } + + }; + } + + @ResourcePath("ignored") + @Produces + MailTemplate get(InjectionPoint injectionPoint) { + ResourcePath path = null; + for (Annotation qualifier : injectionPoint.getQualifiers()) { + if (qualifier.annotationType().equals(ResourcePath.class)) { + path = (ResourcePath) qualifier; + break; + } + } + if (path == null || path.value().isEmpty()) { + throw new IllegalStateException("No template reource path specified"); + } + final String name = path.value(); + return new MailTemplate() { + @Override + public MailTemplateInstance instance() { + return new MailTemplateInstanceImpl(mailer, template.select(new ResourcePath.Literal(name)).get().instance()); + } + }; + } +} diff --git a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java index 73f794af8c466..b289520a3ff13 100644 --- a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java +++ b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java @@ -48,7 +48,7 @@ static void startWiser() { @AfterAll static void stopWiser() { wiser.stop(); - vertx.close(); + vertx.close().toCompletableFuture().join(); } @BeforeEach diff --git a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java index 1df77c571bf40..e10a92f73a42a 100644 --- a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java +++ b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java @@ -32,7 +32,7 @@ static void start() { @AfterAll static void stop() { - vertx.close(); + vertx.close().toCompletableFuture().join(); } @BeforeEach diff --git a/extensions/mongodb-client/deployment/pom.xml b/extensions/mongodb-client/deployment/pom.xml index 2b8116cc131ee..f7882d5c3a20d 100644 --- a/extensions/mongodb-client/deployment/pom.xml +++ b/extensions/mongodb-client/deployment/pom.xml @@ -30,6 +30,10 @@ io.quarkus quarkus-vertx-deployment + + io.quarkus + quarkus-smallrye-health-spi + io.quarkus quarkus-mongodb-client diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientBuildTimeConfig.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientBuildTimeConfig.java new file mode 100644 index 0000000000000..8352dcaab7b9a --- /dev/null +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientBuildTimeConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.mongodb.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "mongodb", phase = ConfigPhase.BUILD_TIME) +public class MongoClientBuildTimeConfig { + /** + * Whether or not an health check is published in case the smallrye-health extension is present. + */ + @ConfigItem(name = "health.enabled", defaultValue = "true") + public boolean healthEnabled; +} diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java index 815f49a604951..7d60b6efdc66a 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java @@ -27,6 +27,7 @@ import io.quarkus.mongodb.runtime.MongoClientProducer; import io.quarkus.mongodb.runtime.MongoClientRecorder; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; public class MongoClientProcessor { @@ -68,4 +69,10 @@ MongoClientBuildItem build(BuildProducer feature, MongoClientR RuntimeValue reactiveClient = recorder.configureTheReactiveClient(); return new MongoClientBuildItem(client, reactiveClient); } + + @BuildStep + HealthBuildItem addHealthCheck(MongoClientBuildTimeConfig buildTimeConfig) { + return new HealthBuildItem("io.quarkus.mongodb.health.MongoHealthCheck", + buildTimeConfig.healthEnabled, "mongodb"); + } } diff --git a/extensions/mongodb-client/runtime/pom.xml b/extensions/mongodb-client/runtime/pom.xml index 43e6567abf057..5324fc3b2013d 100644 --- a/extensions/mongodb-client/runtime/pom.xml +++ b/extensions/mongodb-client/runtime/pom.xml @@ -13,6 +13,7 @@ quarkus-mongodb-client Quarkus - MongoDB Client - Runtime An imperative and reactive client for MongoDB + @@ -29,13 +30,20 @@ org.mongodb - mongo-java-driver + mongodb-driver-sync org.mongodb mongodb-driver-reactivestreams + + + io.quarkus + quarkus-smallrye-health + true + + com.oracle.substratevm svm diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java new file mode 100644 index 0000000000000..a17bfdf4bf6d4 --- /dev/null +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java @@ -0,0 +1,35 @@ +package io.quarkus.mongodb.health; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; + +import com.mongodb.client.MongoClient; + +@Readiness +@ApplicationScoped +public class MongoHealthCheck implements HealthCheck { + @Inject + MongoClient mongoClient; + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.named("MongoDB connection health check").up(); + try { + StringBuilder databases = new StringBuilder(); + for (String db : mongoClient.listDatabaseNames()) { + if (databases.length() != 0) { + databases.append(", "); + } + databases.append(db); + } + return builder.withData("databases", databases.toString()).build(); + } catch (Exception e) { + return builder.down().withData("reason", e.getMessage()).build(); + } + } +} diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientConfig.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientConfig.java index 32e46e5641010..1ef09c8208c77 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientConfig.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientConfig.java @@ -63,10 +63,10 @@ public class MongoClientConfig { public Optional connectionString; /** - * Configures the Mongo server addressed (one if single mode). - * The addressed are passed as {@code host:port}. + * Configures the MongoDB server addressed (one if single mode). + * The addresses are passed as {@code host:port}. */ - @ConfigItem + @ConfigItem(defaultValue = "127.0.0.1:27017") public List hosts; /** diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java index 823595b12f18e..9d75ecdaa7429 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java @@ -79,7 +79,7 @@ private void close() { } void initialize(MongoClientConfig config, List codecProviders) { - CodecRegistry defaultCodecRegistry = com.mongodb.MongoClient.getDefaultCodecRegistry(); + CodecRegistry defaultCodecRegistry = MongoClientSettings.getDefaultCodecRegistry(); MongoClientSettings.Builder settings = MongoClientSettings.builder(); diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/graal/MongoClientSubstitutions.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/graal/MongoClientSubstitutions.java index 6117438639628..b0134e2bdff3c 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/graal/MongoClientSubstitutions.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/graal/MongoClientSubstitutions.java @@ -114,25 +114,6 @@ private CompressorSubstitute createCompressor(final MongoCompressor mongoCompres } } -@TargetClass(MongoClientOptions.class) -final class MongoClientOptionsSubstitution { - - @Alias - private SocketFactory socketFactory; - - @Alias - private static SocketFactory DEFAULT_SOCKET_FACTORY; - - @Substitute - public SocketFactory getSocketFactory() { - if (this.socketFactory != null) { - return this.socketFactory; - } else { - return DEFAULT_SOCKET_FACTORY; - } - } -} - @TargetClass(UnixSocketChannelStream.class) @Delete final class UnixSocketChannelStreamSubstitution { diff --git a/extensions/mongodb-client/runtime/src/test/java/io/quarkus/mongodb/MongoWithReplicasTestBase.java b/extensions/mongodb-client/runtime/src/test/java/io/quarkus/mongodb/MongoWithReplicasTestBase.java index 179172320ccdf..4c3a49bba58f6 100644 --- a/extensions/mongodb-client/runtime/src/test/java/io/quarkus/mongodb/MongoWithReplicasTestBase.java +++ b/extensions/mongodb-client/runtime/src/test/java/io/quarkus/mongodb/MongoWithReplicasTestBase.java @@ -100,7 +100,8 @@ private static void initializeReplicaSet(final List mongodConfigL // Check replica set status before to proceed await() - .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(1, TimeUnit.MINUTES) .until(() -> { Document result = mongoAdminDB.runCommand(new Document("replSetGetStatus", 1)); LOGGER.infof("replSetGetStatus: %s", result); diff --git a/extensions/narayana-jta/deployment/pom.xml b/extensions/narayana-jta/deployment/pom.xml index 1346085f34917..3b236dcde231d 100644 --- a/extensions/narayana-jta/deployment/pom.xml +++ b/extensions/narayana-jta/deployment/pom.xml @@ -26,6 +26,11 @@ io.quarkus quarkus-narayana-jta + + io.quarkus + quarkus-junit5-internal + test + diff --git a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java index 02387f0f90e63..805d99e651568 100644 --- a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java +++ b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java @@ -41,11 +41,6 @@ class NarayanaJtaProcessor { - /** - * The transactions configuration. - */ - TransactionManagerConfiguration transactions; - @BuildStep public NativeImageSystemPropertyBuildItem nativeImageSystemPropertyBuildItem() { return new NativeImageSystemPropertyBuildItem("CoordinatorEnvironmentBean.transactionStatusManagerEnable", "false"); @@ -62,7 +57,8 @@ public void build(NarayanaJtaRecorder recorder, BuildProducer additionalBeans, BuildProducer reflectiveClass, BuildProducer runtimeInit, - BuildProducer feature) { + BuildProducer feature, + TransactionManagerConfiguration transactions) { feature.produce(new FeatureBuildItem(FeatureBuildItem.NARAYANA_JTA)); additionalBeans.produce(new AdditionalBeanBuildItem(NarayanaJtaProducers.class)); additionalBeans.produce(new AdditionalBeanBuildItem(CDIDelegatingTransactionManager.class)); diff --git a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/observers/TransactionalObserversErrorHandlingTest.java b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/observers/TransactionalObserversErrorHandlingTest.java new file mode 100644 index 0000000000000..17eab04cbc09f --- /dev/null +++ b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/observers/TransactionalObserversErrorHandlingTest.java @@ -0,0 +1,61 @@ +package io.quarkus.narayana.observers; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.event.TransactionPhase; +import javax.inject.Inject; +import javax.transaction.UserTransaction; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests that when an observer throws an exception, this doesn't crash the application or prevents other observers from + * being notified. + */ +public class TransactionalObserversErrorHandlingTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ObservingBean.class)); + + @Inject + UserTransaction tx; + + @Inject + Event event; + + @Test + public void testObserverNotificationWithErrorThrowing() throws Exception { + ObservingBean.TIMES_NOTIFIED = 0; + tx.begin(); + event.fire("foo"); + Assertions.assertTrue(ObservingBean.TIMES_NOTIFIED == 0); + tx.commit(); + Assertions.assertTrue(ObservingBean.TIMES_NOTIFIED == 2); + } + + @ApplicationScoped + static class ObservingBean { + + public static int TIMES_NOTIFIED = 0; + + public void observeAfterSuccess(@Observes(during = TransactionPhase.AFTER_SUCCESS) String payload) { + TIMES_NOTIFIED++; + throw new IllegalStateException("This is an expected exception within test"); + } + + public void observeAfterSuccess2(@Observes(during = TransactionPhase.AFTER_SUCCESS) String payload) { + TIMES_NOTIFIED++; + throw new IllegalStateException("This is an expected exception within test"); + + } + } +} diff --git a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/observers/TransactionalObserversTest.java b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/observers/TransactionalObserversTest.java new file mode 100644 index 0000000000000..822436194cda0 --- /dev/null +++ b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/observers/TransactionalObserversTest.java @@ -0,0 +1,178 @@ +package io.quarkus.narayana.observers; + +import java.util.ArrayList; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.event.TransactionPhase; +import javax.inject.Inject; +import javax.transaction.UserTransaction; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests that Arc transactional observers work with Narayana-provided Synchronization registry. + * All observers also make use of Request scoped bean so that we verify that the context is automatically activated. + */ +public class TransactionalObserversTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ObservingBean.class, Actions.class)); + + public static String AFTER_SUCCESS = "AFTER_SUCCESS"; + public static String AFTER_COMPLETION = "AFTER_COMPLETION"; + public static String AFTER_FAILURE = "AFTER_FAILURE"; + public static String BEFORE_COMPLETION = "BEFORE_COMPLETION"; + public static String PLAIN = "PLAIN"; + + @BeforeEach + public void before() { + Actions.clear(); + } + + @Inject + UserTransaction tx; + + @Inject + Event event; + + @Test + public void testTransactionSuccessful() throws Exception { + tx.begin(); + event.fire("commit"); + Assertions.assertTrue(Actions.getActions().size() == 1); + Assertions.assertTrue(Actions.contains(TransactionalObserversTest.PLAIN)); + tx.commit(); + List actions = Actions.getActions(); + Assertions.assertTrue(actions.size() == 4); + Actions.contains(TransactionalObserversTest.AFTER_COMPLETION, TransactionalObserversTest.AFTER_SUCCESS, + TransactionalObserversTest.BEFORE_COMPLETION); + Actions.precedes(TransactionalObserversTest.BEFORE_COMPLETION, TransactionalObserversTest.AFTER_COMPLETION, + TransactionalObserversTest.AFTER_SUCCESS); + } + + @Test + public void testTransactionFailed() throws Exception { + tx.begin(); + event.fire("rollback"); + Assertions.assertTrue(Actions.getActions().size() == 1); + Assertions.assertTrue(Actions.contains(TransactionalObserversTest.PLAIN)); + tx.rollback(); + Assertions.assertTrue(Actions.getActions().size() == 3); + Actions.contains(TransactionalObserversTest.AFTER_COMPLETION, TransactionalObserversTest.AFTER_FAILURE); + } + + @Test + public void testOutsideTransaction() { + event.fire("outsideTx"); + Assertions.assertTrue(Actions.getActions().size() == 5); + Actions.contains(TransactionalObserversTest.AFTER_COMPLETION, TransactionalObserversTest.AFTER_FAILURE, + TransactionalObserversTest.BEFORE_COMPLETION, TransactionalObserversTest.AFTER_SUCCESS, + TransactionalObserversTest.PLAIN); + } + + @ApplicationScoped + static class ObservingBean { + + public void observeAfterSuccess(@Observes(during = TransactionPhase.AFTER_SUCCESS) String payload, ReqScopedBean bean) { + Actions.add(TransactionalObserversTest.AFTER_SUCCESS); + bean.ping(); + } + + public void observeAfterFailure(@Observes(during = TransactionPhase.AFTER_FAILURE) String payload, ReqScopedBean bean) { + Actions.add(TransactionalObserversTest.AFTER_FAILURE); + bean.ping(); + } + + public void observeAfterCompletion(@Observes(during = TransactionPhase.AFTER_COMPLETION) String payload, + ReqScopedBean bean) { + Actions.add(TransactionalObserversTest.AFTER_COMPLETION); + bean.ping(); + } + + public void observeBeforeCompletion(@Observes(during = TransactionPhase.BEFORE_COMPLETION) String payload, + ReqScopedBean bean) { + Actions.add(TransactionalObserversTest.BEFORE_COMPLETION); + bean.ping(); + } + + public void classicObserver(@Observes String payload, ReqScopedBean bean) { + Actions.add(TransactionalObserversTest.PLAIN); + bean.ping(); + } + } + + @RequestScoped + static class ReqScopedBean { + // just to verify that the context gets activated for OMs + public void ping() { + } + } + + static class Actions { + + private static List actions = new ArrayList(); + + public static List getActions() { + return actions; + } + + public static void clear() { + actions.clear(); + } + + public static boolean add(Object o) { + return actions.add(o.toString()); + } + + public static boolean isSequence(Object... seq) { + int i = 0; + return objectsToStrings(seq).equals(actions); + } + + // true iff obj exists and all otherObjects exist and indexOf(obj) < indexOf(x) for each x from otherObjects + public static boolean precedes(Object obj, Object... otherObjects) { + boolean precedes = true; + int i = 0; + if (precedes = (Actions.contains(obj) && Actions.contains(otherObjects))) { + while (i < otherObjects.length && (precedes = precedes + && actions.indexOf(obj.toString()) < actions.indexOf(otherObjects[i++].toString()))) + ; + } + return precedes; + } + + public static boolean startsWith(Object... objects) { + return actions.subList(0, objects.length).equals(objectsToStrings(objects)); + } + + public static boolean endsWith(Object... objects) { + return actions.subList(actions.size() - objects.length, actions.size()).equals(objectsToStrings(objects)); + } + + public static boolean contains(Object... objects) { + return actions.containsAll(objectsToStrings(objects)); + } + + private static List objectsToStrings(final Object... objects) { + List result = new ArrayList(); + for (Object obj : objects) { + result.add(obj.toString()); + } + return result; + } + } + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java index 419d68bbb9255..c3a3070e3b681 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java @@ -14,6 +14,8 @@ import com.arjuna.ats.jbossatx.jta.RecoveryManagerService; import com.arjuna.ats.jta.UserTransaction; +import io.quarkus.arc.Unremovable; + @Dependent public class NarayanaJtaProducers { private static final javax.transaction.UserTransaction USER_TRANSACTION = UserTransaction.userTransaction(); @@ -38,6 +40,7 @@ public XAResourceRecoveryRegistry xaResourceRecoveryRegistry() { @Produces @ApplicationScoped + @Unremovable // needed by Arc for transactional observers public TransactionSynchronizationRegistry transactionSynchronizationRegistry() { return new TransactionSynchronizationRegistryImple(); } diff --git a/extensions/narayana-stm/deployment/pom.xml b/extensions/narayana-stm/deployment/pom.xml index 134fe3cf1705c..316384f4f324c 100644 --- a/extensions/narayana-stm/deployment/pom.xml +++ b/extensions/narayana-stm/deployment/pom.xml @@ -25,7 +25,6 @@ io.quarkus quarkus-narayana-stm - ${project.version} diff --git a/extensions/narayana-stm/runtime/pom.xml b/extensions/narayana-stm/runtime/pom.xml index 0c02b3932bff3..86d9825403c82 100644 --- a/extensions/narayana-stm/runtime/pom.xml +++ b/extensions/narayana-stm/runtime/pom.xml @@ -22,6 +22,10 @@ io.quarkus quarkus-core + + com.oracle.substratevm + svm + diff --git a/extensions/narayana-stm/runtime/src/main/java/io/quarkus/narayana/stm/runtime/ObjectStoreEnvironmentBeanSubstitution.java b/extensions/narayana-stm/runtime/src/main/java/io/quarkus/narayana/stm/runtime/ObjectStoreEnvironmentBeanSubstitution.java new file mode 100644 index 0000000000000..6cba62ed78c27 --- /dev/null +++ b/extensions/narayana-stm/runtime/src/main/java/io/quarkus/narayana/stm/runtime/ObjectStoreEnvironmentBeanSubstitution.java @@ -0,0 +1,18 @@ +package io.quarkus.narayana.stm.runtime; + +import java.io.File; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean") +public final class ObjectStoreEnvironmentBeanSubstitution { + + /** + * @return fixed ObjectStore path resolved during runtime + */ + @Substitute + public String getObjectStoreDir() { + return System.getProperty("user.home") + File.separator + "ObjectStore"; + } +} diff --git a/extensions/neo4j/deployment/pom.xml b/extensions/neo4j/deployment/pom.xml index de4f2d59f5af3..9ec3f78fed856 100644 --- a/extensions/neo4j/deployment/pom.xml +++ b/extensions/neo4j/deployment/pom.xml @@ -21,6 +21,10 @@ io.quarkus quarkus-arc-deployment + + io.quarkus + quarkus-smallrye-health-spi + io.quarkus quarkus-neo4j diff --git a/extensions/neo4j/deployment/src/main/java/io/quarkus/neo4j/deployment/Neo4jBuildTimeConfig.java b/extensions/neo4j/deployment/src/main/java/io/quarkus/neo4j/deployment/Neo4jBuildTimeConfig.java new file mode 100644 index 0000000000000..c968d7f59eae6 --- /dev/null +++ b/extensions/neo4j/deployment/src/main/java/io/quarkus/neo4j/deployment/Neo4jBuildTimeConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.neo4j.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "neo4j", phase = ConfigPhase.BUILD_TIME) +public class Neo4jBuildTimeConfig { + /** + * Whether or not an health check is published in case the smallrye-health extension is present. + */ + @ConfigItem(name = "health.enabled", defaultValue = "true") + public boolean healthEnabled; +} diff --git a/extensions/neo4j/deployment/src/main/java/io/quarkus/neo4j/deployment/Neo4jDriverProcessor.java b/extensions/neo4j/deployment/src/main/java/io/quarkus/neo4j/deployment/Neo4jDriverProcessor.java index f4f7b2e047a9d..e128aa1f39b4e 100644 --- a/extensions/neo4j/deployment/src/main/java/io/quarkus/neo4j/deployment/Neo4jDriverProcessor.java +++ b/extensions/neo4j/deployment/src/main/java/io/quarkus/neo4j/deployment/Neo4jDriverProcessor.java @@ -12,6 +12,7 @@ import io.quarkus.neo4j.runtime.Neo4jConfiguration; import io.quarkus.neo4j.runtime.Neo4jDriverProducer; import io.quarkus.neo4j.runtime.Neo4jDriverRecorder; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; class Neo4jDriverProcessor { @@ -37,4 +38,10 @@ void configureDriverProducer(Neo4jDriverRecorder recorder, BeanContainerBuildIte recorder.configureNeo4jProducer(beanContainerBuildItem.getValue(), configuration, shutdownContext); } + + @BuildStep + HealthBuildItem addHealthCheck(Neo4jBuildTimeConfig buildTimeConfig) { + return new HealthBuildItem("io.quarkus.neo4j.runtime.health.Neo4jHealthCheck", + buildTimeConfig.healthEnabled, "neo4j"); + } } diff --git a/extensions/neo4j/runtime/pom.xml b/extensions/neo4j/runtime/pom.xml index a9fde15ef64ef..7f824f086e30f 100644 --- a/extensions/neo4j/runtime/pom.xml +++ b/extensions/neo4j/runtime/pom.xml @@ -21,6 +21,11 @@ io.quarkus quarkus-arc + + io.quarkus + quarkus-smallrye-health + true + com.oracle.substratevm svm diff --git a/extensions/neo4j/runtime/src/main/java/io/quarkus/neo4j/runtime/health/Neo4jHealthCheck.java b/extensions/neo4j/runtime/src/main/java/io/quarkus/neo4j/runtime/health/Neo4jHealthCheck.java new file mode 100644 index 0000000000000..caa1bc77e6fdf --- /dev/null +++ b/extensions/neo4j/runtime/src/main/java/io/quarkus/neo4j/runtime/health/Neo4jHealthCheck.java @@ -0,0 +1,95 @@ +package io.quarkus.neo4j.runtime.health; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; +import org.jboss.logging.Logger; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.summary.ServerInfo; + +@Readiness +@ApplicationScoped +public class Neo4jHealthCheck implements HealthCheck { + + private static final Logger log = Logger.getLogger(Neo4jHealthCheck.class); + + /** + * The Cypher statement used to verify Neo4j is up. + */ + private static final String CYPHER = "RETURN 1 AS result"; + /** + * Message indicating that the health check failed. + */ + private static final String MESSAGE_HEALTH_CHECK_FAILED = "Neo4j health check failed"; + /** + * Message logged before retrying a health check. + */ + private static final String MESSAGE_SESSION_EXPIRED = "Neo4j session has expired, retrying one single time to retrieve server health."; + /** + * The default session config to use while connecting. + */ + private static final SessionConfig DEFAULT_SESSION_CONFIG = SessionConfig.builder() + .withDefaultAccessMode(AccessMode.WRITE) + .build(); + + @Inject + Driver driver; + + @Override + public HealthCheckResponse call() { + + HealthCheckResponseBuilder builder = HealthCheckResponse.named("Neo4j connection health check").up(); + try { + ResultSummary resultSummary; + // Retry one time when the session has been expired + try { + resultSummary = runHealthCheckQuery(); + } catch (SessionExpiredException sessionExpiredException) { + log.warn(MESSAGE_SESSION_EXPIRED); + resultSummary = runHealthCheckQuery(); + } + return buildStatusUp(resultSummary, builder); + } catch (Exception e) { + return builder.down().withData("reason", e.getMessage()).build(); + } + } + + /** + * Applies the given {@link ResultSummary} to the {@link HealthCheckResponseBuilder builder} and calls {@code build} + * afterwards. + * + * @param resultSummary the result summary returned by the server + * @param builder the health builder to be modified + * @return the final {@link HealthCheckResponse health check response} + */ + private static HealthCheckResponse buildStatusUp(ResultSummary resultSummary, HealthCheckResponseBuilder builder) { + ServerInfo serverInfo = resultSummary.server(); + + builder.withData("server", serverInfo.version() + "@" + serverInfo.address()); + + String databaseName = resultSummary.database().name(); + if (!(databaseName == null || databaseName.trim().isEmpty())) { + builder.withData("database", databaseName.trim()); + } + + return builder.build(); + } + + private ResultSummary runHealthCheckQuery() { + // We use WRITE here to make sure UP is returned for a server that supports + // all possible workloads + try (Session session = this.driver.session(DEFAULT_SESSION_CONFIG)) { + ResultSummary resultSummary = session.run(CYPHER).consume(); + return resultSummary; + } + } +} diff --git a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java index efa77cc618b72..b3c4f28ad882d 100644 --- a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java +++ b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java @@ -377,6 +377,18 @@ private static boolean isSkippable(final Class handlerType, final String meth } } +@TargetClass(className = "io.netty.util.internal.NativeLibraryLoader") +final class Target_io_netty_util_internal_NativeLibraryLoader { + + // This method can trick GraalVM into thinking that Classloader#defineClass is getting called + @Substitute + static Class tryToLoadClass(final ClassLoader loader, final Class helper) + throws ClassNotFoundException { + return Class.forName(helper.getName(), false, loader); + } + +} + class NettySubstitutions { } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index 18503bda8ac3b..9f59c6bd3c985 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.oidc.deployment; +import java.util.function.BooleanSupplier; + import org.eclipse.microprofile.jwt.Claim; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -12,6 +14,8 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.oidc.runtime.BearerAuthenticationMechanism; import io.quarkus.oidc.runtime.CodeAuthenticationMechanism; +import io.quarkus.oidc.runtime.DefaultTenantConfigResolver; +import io.quarkus.oidc.runtime.OidcBuildTimeConfig; import io.quarkus.oidc.runtime.OidcConfig; import io.quarkus.oidc.runtime.OidcIdentityProvider; import io.quarkus.oidc.runtime.OidcJsonWebTokenProducer; @@ -25,14 +29,16 @@ @SuppressWarnings("deprecation") public class OidcBuildStep { - @BuildStep + OidcBuildTimeConfig buildTimeConfig; + + @BuildStep(onlyIf = IsEnabled.class) FeatureBuildItem featureBuildItem() { return new FeatureBuildItem(FeatureBuildItem.OIDC); } - @BuildStep - AdditionalBeanBuildItem jwtClaimIntegration(Capabilities capabilities, OidcConfig config) { - if (!capabilities.isCapabilityPresent(Capabilities.JWT) && config.enabled) { + @BuildStep(onlyIf = IsEnabled.class) + AdditionalBeanBuildItem jwtClaimIntegration(Capabilities capabilities) { + if (!capabilities.isCapabilityPresent(Capabilities.JWT)) { AdditionalBeanBuildItem.Builder removable = AdditionalBeanBuildItem.builder(); removable.addBeanClass(CommonJwtProducer.class); removable.addBeanClass(RawClaimTypeProducer.class); @@ -43,35 +49,38 @@ AdditionalBeanBuildItem jwtClaimIntegration(Capabilities capabilities, OidcConfi return null; } - @BuildStep - public AdditionalBeanBuildItem beans(OidcConfig config) { - if (config.enabled) { - AdditionalBeanBuildItem.Builder beans = AdditionalBeanBuildItem.builder().setUnremovable(); + @BuildStep(onlyIf = IsEnabled.class) + public AdditionalBeanBuildItem beans() { + AdditionalBeanBuildItem.Builder beans = AdditionalBeanBuildItem.builder().setUnremovable(); - if (OidcConfig.ApplicationType.SERVICE.equals(config.getApplicationType())) { - beans.addBeanClass(BearerAuthenticationMechanism.class); - } else if (OidcConfig.ApplicationType.WEB_APP.equals(config.getApplicationType())) { - beans.addBeanClass(CodeAuthenticationMechanism.class); - } - return beans.addBeanClass(OidcJsonWebTokenProducer.class) - .addBeanClass(OidcTokenCredentialProducer.class) - .addBeanClass(OidcIdentityProvider.class).build(); + if (OidcBuildTimeConfig.ApplicationType.SERVICE.equals(buildTimeConfig.applicationType)) { + beans.addBeanClass(BearerAuthenticationMechanism.class); + } else if (OidcBuildTimeConfig.ApplicationType.WEB_APP.equals(buildTimeConfig.applicationType)) { + beans.addBeanClass(CodeAuthenticationMechanism.class); } - - return null; + return beans.addBeanClass(OidcJsonWebTokenProducer.class) + .addBeanClass(OidcTokenCredentialProducer.class) + .addBeanClass(OidcIdentityProvider.class) + .addBeanClass(DefaultTenantConfigResolver.class).build(); } - @BuildStep + @BuildStep(onlyIf = IsEnabled.class) EnableAllSecurityServicesBuildItem security() { return new EnableAllSecurityServicesBuildItem(); } @Record(ExecutionTime.RUNTIME_INIT) - @BuildStep + @BuildStep(onlyIf = IsEnabled.class) public void setup(OidcConfig config, OidcRecorder recorder, InternalWebVertxBuildItem vertxBuildItem, BeanContainerBuildItem bc) { - if (config.enabled) { - recorder.setup(config, vertxBuildItem.getVertx(), bc.getValue()); + recorder.setup(config, vertxBuildItem.getVertx(), bc.getValue()); + } + + static class IsEnabled implements BooleanSupplier { + OidcBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.enabled; } } } diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml index 9e01a62e36ec5..df7ee0169517d 100644 --- a/extensions/oidc/runtime/pom.xml +++ b/extensions/oidc/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc Quarkus - OpenID Connect Adapter - Runtime - Secure your applications with OpenID Connect and Keycloak + Secure your applications with OpenID Connect Adapter and IDP such as Keycloak io.quarkus diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AccessTokenCredential.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AccessTokenCredential.java index e7e6c8cae2385..461e991e0734a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AccessTokenCredential.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AccessTokenCredential.java @@ -1,13 +1,14 @@ package io.quarkus.oidc; -import io.quarkus.security.credential.TokenCredential; +import io.quarkus.oidc.runtime.ContextAwareTokenCredential; +import io.vertx.ext.web.RoutingContext; -public class AccessTokenCredential extends TokenCredential { +public class AccessTokenCredential extends ContextAwareTokenCredential { private RefreshToken refreshToken; public AccessTokenCredential() { - this(null); + this(null, null); } /** @@ -15,8 +16,8 @@ public AccessTokenCredential() { * * @param accessToken - access token */ - public AccessTokenCredential(String accessToken) { - this(accessToken, null); + public AccessTokenCredential(String accessToken, RoutingContext context) { + super(accessToken, "bearer", context); } /** @@ -25,8 +26,8 @@ public AccessTokenCredential(String accessToken) { * @param accessToken - access token * @param refreshToken - refresh token which can be used to refresh this access token, may be null */ - public AccessTokenCredential(String accessToken, RefreshToken refreshToken) { - super(accessToken, "bearer"); + public AccessTokenCredential(String accessToken, RefreshToken refreshToken, RoutingContext context) { + this(accessToken, context); this.refreshToken = refreshToken; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/IdTokenCredential.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/IdTokenCredential.java index 6998903e0708b..6f2846132c842 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/IdTokenCredential.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/IdTokenCredential.java @@ -1,13 +1,15 @@ package io.quarkus.oidc; -import io.quarkus.security.credential.TokenCredential; +import io.quarkus.oidc.runtime.ContextAwareTokenCredential; +import io.vertx.ext.web.RoutingContext; + +public class IdTokenCredential extends ContextAwareTokenCredential { -public class IdTokenCredential extends TokenCredential { public IdTokenCredential() { - this(null); + this(null, null); } - public IdTokenCredential(String token) { - super(token, "id_token"); + public IdTokenCredential(String token, RoutingContext context) { + super(token, "id_token", context); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java new file mode 100644 index 0000000000000..214d59e7898a0 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java @@ -0,0 +1,24 @@ +package io.quarkus.oidc; + +import io.quarkus.oidc.runtime.OidcTenantConfig; +import io.vertx.ext.web.RoutingContext; + +/** + *

+ * A tenant resolver is responsible for resolving the {@link OidcTenantConfig} for tenants, dynamically. + * + *

+ * Instead of implementing a {@link TenantResolver} that maps the tenant configuration based on an identifier and its + * corresponding entry in the application configuration file, beans implementing this interface can dynamically construct the + * tenant configuration without having to define each tenant in the application configuration file. + */ +public interface TenantConfigResolver { + + /** + * Returns a {@link OidcTenantConfig} given a {@code RoutingContext}. + * + * @param context the routing context + * @return the tenant configuration. If {@code null}, indicates that the default configuration/tenant should be chosen + */ + OidcTenantConfig resolve(RoutingContext context); +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantResolver.java new file mode 100644 index 0000000000000..6d8a5067bbf00 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantResolver.java @@ -0,0 +1,18 @@ +package io.quarkus.oidc; + +import io.vertx.ext.web.RoutingContext; + +/** + * A tenant resolver is responsible for resolving tenants dynamically so that the proper configuration can be used accordingly. + */ +public interface TenantResolver { + + /** + * Returns a tenant identifier given a {@code RoutingContext}, where the identifier will be used to choose the proper + * configuration during runtime. + * + * @param context the routing context + * @return the tenant identifier. If {@code null}, indicates that the default configuration/tenant should be chosen + */ + String resolve(RoutingContext context); +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java index 137d72b9640ab..78bccec7aa2f3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java @@ -2,25 +2,20 @@ import java.util.concurrent.CompletionStage; +import javax.inject.Inject; + import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TokenAuthenticationRequest; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; -import io.vertx.ext.auth.oauth2.OAuth2Auth; abstract class AbstractOidcAuthenticationMechanism implements HttpAuthenticationMechanism { protected static final String BEARER = "Bearer"; - protected volatile OAuth2Auth auth; - protected OidcConfig config; - - public AbstractOidcAuthenticationMechanism setAuth(OAuth2Auth auth, OidcConfig config) { - this.auth = auth; - this.config = config; - return this; - } + @Inject + DefaultTenantConfigResolver tenantConfigResolver; protected CompletionStage authenticate(IdentityProviderManager identityProviderManager, TokenCredential token) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java index bbc170a2aff06..4570af8484040 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java @@ -17,13 +17,14 @@ @ApplicationScoped public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMechanism { + @Override public CompletionStage authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { String token = extractBearerToken(context); // if a bearer token is provided try to authenticate if (token != null) { - return authenticate(identityProviderManager, new AccessTokenCredential(token)); + return authenticate(identityProviderManager, new AccessTokenCredential(token, context)); } return CompletableFuture.completedFuture(null); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index 8c6c060b8872a..5ada73aa2ec59 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -11,6 +11,8 @@ import javax.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; + import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.IdTokenCredential; @@ -19,6 +21,7 @@ import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.AuthenticationRedirectException; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpHeaders; @@ -31,18 +34,21 @@ @ApplicationScoped public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism { + private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class); + private static final String STATE_COOKIE_NAME = "q_auth"; private static final String SESSION_COOKIE_NAME = "q_session"; - private static final String SESSION_COOKIE_DELIM = "___"; + private static final String COOKIE_DELIM = "___"; private static QuarkusSecurityIdentity augmentIdentity(SecurityIdentity securityIdentity, String accessToken, - String refreshToken) { + String refreshToken, + RoutingContext context) { final RefreshToken refreshTokenCredential = new RefreshToken(refreshToken); return QuarkusSecurityIdentity.builder() .setPrincipal(securityIdentity.getPrincipal()) .addCredentials(securityIdentity.getCredentials()) - .addCredential(new AccessTokenCredential(accessToken, refreshTokenCredential)) + .addCredential(new AccessTokenCredential(accessToken, refreshTokenCredential, context)) .addCredential(refreshTokenCredential) .addRoles(securityIdentity.getRoles()) .addAttributes(securityIdentity.getAttributes()) @@ -62,12 +68,13 @@ public CompletionStage authenticate(RoutingContext context, // if session already established, try to re-authenticate if (sessionCookie != null) { - String[] tokens = sessionCookie.getValue().split(SESSION_COOKIE_DELIM); - return authenticate(identityProviderManager, new IdTokenCredential(tokens[0])) + String[] tokens = sessionCookie.getValue().split(COOKIE_DELIM); + return authenticate(identityProviderManager, new IdTokenCredential(tokens[0], context)) .thenCompose(new Function>() { @Override public CompletionStage apply(SecurityIdentity securityIdentity) { - return CompletableFuture.completedFuture(augmentIdentity(securityIdentity, tokens[1], tokens[2])); + return CompletableFuture + .completedFuture(augmentIdentity(securityIdentity, tokens[1], tokens[2], context)); } }); } @@ -78,21 +85,25 @@ public CompletionStage apply(SecurityIdentity securityIdentity @Override public CompletionStage getChallenge(RoutingContext context) { - removeSessionCookie(context); + removeCookie(context, SESSION_COOKIE_NAME); ChallengeData challenge; - JsonObject params = new JsonObject(); List scopes = new ArrayList<>(); scopes.add("openid"); - scopes.addAll(config.authentication.scopes); + tenantConfigResolver.resolve(context).oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll); params.put("scopes", new JsonArray(scopes)); - params.put("redirect_uri", buildRedirectUri(context)); - params.put("state", generateState(context)); - challenge = new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, auth.authorizeURL(params)); + URI absoluteUri = URI.create(context.request().absoluteURI()); + String dynamicPath = getDynamicPath(context, absoluteUri); + params.put("redirect_uri", buildCodeRedirectUri(context, absoluteUri, dynamicPath)); + + params.put("state", generateState(context, dynamicPath)); + + challenge = new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, + tenantConfigResolver.resolve(context).auth.authorizeURL(params)); return CompletableFuture.completedFuture(challenge); } @@ -105,17 +116,61 @@ private CompletionStage performCodeFlow(IdentityProviderManage if (code == null) { return CompletableFuture.completedFuture(null); } + CompletableFuture cf = new CompletableFuture<>(); + + URI absoluteUri = URI.create(context.request().absoluteURI()); + + Cookie stateCookie = context.getCookie(STATE_COOKIE_NAME); + if (stateCookie != null) { + List values = context.queryParam("state"); + // IDP must return a 'state' query parameter and the value of the state cookie must start with this parameter's value + if (values.size() != 1 || !stateCookie.getValue().startsWith(values.get(0))) { + cf.completeExceptionally(new AuthenticationFailedException()); + return cf; + } else if (context.queryParam("pathChecked").isEmpty()) { + // This is an original redirect from IDP, check if the request path needs to be updated + String[] pair = stateCookie.getValue().split(COOKIE_DELIM); + if (pair.length == 2) { + // The extra path that needs to be added to the current request path + String extraPath = pair[1]; + // Adding a query marker that the state cookie has already been used to restore the path + // as deleting it now would increase the risk of CSRF + String extraQuery = "?pathChecked=true"; + + // The query parameters returned from IDP need to be included + if (absoluteUri.getRawQuery() != null) { + extraQuery += ("&" + absoluteUri.getRawQuery()); + } + + String localRedirectUri = buildLocalRedirectUri(context, absoluteUri, extraPath + extraQuery); + LOG.debug("Local redirectUri: " + localRedirectUri); + + cf.completeExceptionally(new AuthenticationRedirectException(localRedirectUri)); + return cf; + } + // The redirect path matches the original request path, the state cookie is no longer needed + removeCookie(context, STATE_COOKIE_NAME); + } else { + // Local redirect restoring the original request path, the state cookie is no longer needed + removeCookie(context, STATE_COOKIE_NAME); + } + } else { + // State cookie must be available to minimize the risk of CSRF + cf.completeExceptionally(new AuthenticationFailedException()); + return cf; + } + params.put("code", code); - params.put("redirect_uri", buildRedirectUri(context)); + params.put("redirect_uri", buildCodeRedirectUri(context, absoluteUri, getDynamicPath(context, absoluteUri))); - auth.authenticate(params, userAsyncResult -> { + tenantConfigResolver.resolve(context).auth.authenticate(params, userAsyncResult -> { if (userAsyncResult.failed()) { cf.completeExceptionally(new AuthenticationFailedException()); } else { AccessToken result = AccessToken.class.cast(userAsyncResult.result()); - authenticate(identityProviderManager, new IdTokenCredential(result.opaqueIdToken())) + authenticate(identityProviderManager, new IdTokenCredential(result.opaqueIdToken(), context)) .whenCompleteAsync((securityIdentity, throwable) -> { if (throwable != null) { cf.completeExceptionally(throwable); @@ -129,46 +184,75 @@ private CompletionStage performCodeFlow(IdentityProviderManage return cf; } - private void processSuccessfulAuthentication(RoutingContext context, CompletableFuture cf, + private void processSuccessfulAuthentication(RoutingContext context, + CompletableFuture cf, AccessToken result, SecurityIdentity securityIdentity) { - removeSessionCookie(context); + removeCookie(context, SESSION_COOKIE_NAME); CookieImpl cookie = new CookieImpl(SESSION_COOKIE_NAME, new StringBuilder(result.opaqueIdToken()) - .append(SESSION_COOKIE_DELIM) + .append(COOKIE_DELIM) .append(result.opaqueAccessToken()) - .append(SESSION_COOKIE_DELIM) + .append(COOKIE_DELIM) .append(result.opaqueRefreshToken()).toString()); cookie.setMaxAge(result.idToken().getInteger("exp")); cookie.setSecure(context.request().isSSL()); cookie.setHttpOnly(true); - context.response().addCookie(cookie); + cf.complete(augmentIdentity(securityIdentity, result.opaqueAccessToken(), - result.opaqueRefreshToken())); + result.opaqueRefreshToken(), context)); } - private String generateState(RoutingContext context) { - CookieImpl cookie = new CookieImpl(STATE_COOKIE_NAME, UUID.randomUUID().toString()); + private String getDynamicPath(RoutingContext context, URI absoluteUri) { + OidcTenantConfig config = tenantConfigResolver.resolve(context).oidcConfig; + if (config.getAuthentication().redirectPath.isPresent()) { + String redirectPath = config.getAuthentication().redirectPath.get(); + String requestPath = absoluteUri.getRawPath(); + if (requestPath.startsWith(redirectPath) && requestPath.length() > redirectPath.length()) { + return requestPath.substring(redirectPath.length()); + } + } + return null; + } + + private String generateState(RoutingContext context, String dynamicPath) { + String uuid = UUID.randomUUID().toString(); + String cookieValue = uuid; + if (dynamicPath != null) { + cookieValue += (COOKIE_DELIM + dynamicPath); + } + + CookieImpl cookie = new CookieImpl(STATE_COOKIE_NAME, cookieValue); cookie.setHttpOnly(true); cookie.setSecure(context.request().isSSL()); - cookie.setMaxAge(-1); + // max-age is 30 minutes + cookie.setMaxAge(60 * 30); context.response().addCookie(cookie); - - return cookie.getValue(); + return uuid; } - private String buildRedirectUri(RoutingContext context) { - URI absoluteUri = URI.create(context.request().absoluteURI()); + private String buildCodeRedirectUri(RoutingContext context, URI absoluteUri, String dynamicPath) { StringBuilder builder = new StringBuilder(context.request().scheme()).append("://") - .append(absoluteUri.getAuthority()) - .append(absoluteUri.getPath()); + .append(absoluteUri.getAuthority()); - return builder.toString(); + String path = dynamicPath != null + ? tenantConfigResolver.resolve(context).oidcConfig.getAuthentication().redirectPath.get() + : absoluteUri.getRawPath(); + + return builder.append(path).toString(); + } + + private String buildLocalRedirectUri(RoutingContext context, URI absoluteUri, String extraPath) { + return new StringBuilder(context.request().scheme()).append("://") + .append(absoluteUri.getAuthority()) + .append(absoluteUri.getRawPath()) + .append(extraPath) + .toString(); } - private void removeSessionCookie(RoutingContext context) { - context.response().removeCookie(SESSION_COOKIE_NAME, true); + private Cookie removeCookie(RoutingContext context, String cookieName) { + return context.response().removeCookie(cookieName, true); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/ContextAwareTokenCredential.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/ContextAwareTokenCredential.java new file mode 100644 index 0000000000000..6588b24fa0273 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/ContextAwareTokenCredential.java @@ -0,0 +1,18 @@ +package io.quarkus.oidc.runtime; + +import io.quarkus.security.credential.TokenCredential; +import io.vertx.ext.web.RoutingContext; + +public class ContextAwareTokenCredential extends TokenCredential { + + private RoutingContext context; + + protected ContextAwareTokenCredential(String token, String type, RoutingContext context) { + super(token, type); + this.context = context; + } + + RoutingContext getContext() { + return context; + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java new file mode 100644 index 0000000000000..866c154462624 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java @@ -0,0 +1,75 @@ +package io.quarkus.oidc.runtime; + +import java.util.Map; +import java.util.function.Function; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import io.quarkus.oidc.TenantConfigResolver; +import io.quarkus.oidc.TenantResolver; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class DefaultTenantConfigResolver { + + @Inject + Instance tenantResolver; + + @Inject + Instance tenantConfigResolver; + + private Map tenantsConfig; + private TenantConfigContext defaultTenant; + private Function tenantConfigContextFactory; + + TenantConfigContext resolve(RoutingContext context) { + if (tenantConfigResolver.isAmbiguous()) { + throw new IllegalStateException("Multiple " + TenantConfigResolver.class + " beans registered"); + } + + if (tenantConfigResolver.isResolvable()) { + OidcTenantConfig tenantConfig = this.tenantConfigResolver.get().resolve(context); + + if (tenantConfig != null) { + String tenantId = tenantConfig.getClientId() + .orElseThrow(() -> new IllegalStateException("You must provide a client_id")); + TenantConfigContext tenantContext = tenantsConfig.get(tenantId); + + if (tenantContext == null) { + synchronized (this) { + return tenantsConfig.computeIfAbsent(tenantId, + clientId -> tenantConfigContextFactory.apply(tenantConfig)); + } + } + + return tenantContext; + } + } + + String tenant = null; + + if (tenantResolver.isAmbiguous()) { + throw new IllegalStateException("Multiple " + TenantResolver.class + " beans registered"); + } + + if (tenantResolver.isResolvable()) { + tenant = tenantResolver.get().resolve(context); + } + + return tenantsConfig.getOrDefault(tenant, defaultTenant); + } + + void setTenantsConfig(Map tenantsConfig) { + this.tenantsConfig = tenantsConfig; + } + + void setDefaultTenant(TenantConfigContext defaultTenant) { + this.defaultTenant = defaultTenant; + } + + void setTenantConfigContextFactory(Function tenantConfigContextFactory) { + this.tenantConfigContextFactory = tenantConfigContextFactory; + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcBuildTimeConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcBuildTimeConfig.java new file mode 100644 index 0000000000000..92a666d785923 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcBuildTimeConfig.java @@ -0,0 +1,38 @@ +package io.quarkus.oidc.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Build time configuration for OIDC. + */ +@ConfigRoot +public class OidcBuildTimeConfig { + /** + * If the OIDC extension is enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean enabled; + + /** + * The application type, which can be one of the following values from enum {@link ApplicationType}. + */ + @ConfigItem(defaultValue = "service") + public ApplicationType applicationType; + + public enum ApplicationType { + /** + * A {@code WEB_APP} is a client that server pages, usually a frontend application. For this type of client the + * Authorization Code Flow is + * defined as the preferred method for authenticating users. + */ + WEB_APP, + + /** + * A {@code SERVICE} is a client that has a set of protected HTTP resources, usually a backend application following the + * RESTful Architectural Design. For this type of client, the Bearer Authorization method is defined as the preferred + * method for authenticating and authorizing users. + */ + SERVICE + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java index ed478e8260e38..79e22cc1171bd 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java @@ -1,184 +1,27 @@ package io.quarkus.oidc.runtime; -import java.time.Duration; -import java.util.List; -import java.util.Optional; +import java.util.Map; -import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; -@ConfigRoot(phase = ConfigPhase.RUN_TIME) +@ConfigRoot(name = "oidc", phase = ConfigPhase.RUN_TIME) public class OidcConfig { /** - * If the OIDC extension is enabled. + * The default tenant. */ - @ConfigItem(defaultValue = "true") - public boolean enabled; + @ConfigItem(name = ConfigItem.PARENT) + public OidcTenantConfig defaultTenant; /** - * The base URL of the OpenID Connect (OIDC) server, for example, 'https://host:port/auth'. - * All the other OIDC server page and service URLs are derived from this URL. - * Note if you work with Keycloak OIDC server, make sure the base URL is in the following format: - * 'https://host:port/auth/realms/{realm}' where '{realm}' has to be replaced by the name of the Keycloak realm. + * Additional named tenants. */ - @ConfigItem - String authServerUrl; - - /** - * Relative path of the RFC7662 introspection service. - */ - @ConfigItem - Optional introspectionPath; - - /** - * Relative path of the OIDC service returning a JWK set. - */ - @ConfigItem - Optional jwksPath; - - /** - * Public key for the local JWT token verification. - */ - @ConfigItem - Optional publicKey; - - /** - * The client-id of the application. Each application has a client-id that is used to identify the application - */ - @ConfigItem - Optional clientId; - - /** - * The maximum amount of time the adapter will try connecting to the currently unavailable OIDC server for. - * For example, setting it to '20S' will let the adapter keep requesting the connection for up to 20 seconds. - */ - @ConfigItem - public Optional connectionDelay; - - /** - * Configuration to find and parse a custom claim containing the roles information. - */ - @ConfigItem - Roles roles; - - /** - * Credentials which the OIDC adapter will use to authenticate to the OIDC server. - */ - @ConfigItem - Credentials credentials; - - /** - * Different options to configure authorization requests - */ - Authentication authentication; - - /** - * The application type, which can be one of the following values from enum {@link ApplicationType}.. - */ - @ConfigItem(defaultValue = "service") - ApplicationType applicationType; - - public String getAuthServerUrl() { - return authServerUrl; - } - - public Optional getClientId() { - return clientId; - } - - public Credentials getCredentials() { - return credentials; - } - - public Roles getRoles() { - return roles; - } - - public ApplicationType getApplicationType() { - return applicationType; - } - - @ConfigGroup - public static class Credentials { - - /** - * The client secret - */ - @ConfigItem - Optional secret; - - public Optional getSecret() { - return secret; - } - } - - @ConfigGroup - public static class Roles { - - /** - * Path to the claim containing an array of groups. It starts from the top level JWT JSON object and - * can contain multiple segments where each segment represents a JSON object name only, example: "realm/groups". - * This property can be used if a token has no 'groups' claim but has the groups set in a different claim. - */ - @ConfigItem - Optional roleClaimPath; - - /** - * Separator for splitting a string which may contain multiple group values. - * It will only be used if the "role-claim-path" property points to a custom claim whose value is a string. - * A single space will be used by default because the standard 'scope' claim may contain a space separated sequence. - */ - @ConfigItem - Optional roleClaimSeparator; - - public Optional getRoleClaimPath() { - return roleClaimPath; - } - - public Optional getRoleClaimSeparator() { - return roleClaimSeparator; - } - - public static Roles fromClaimPath(String path) { - return fromClaimPathAndSeparator(path, null); - } - - public static Roles fromClaimPathAndSeparator(String path, String sep) { - Roles roles = new Roles(); - roles.roleClaimPath = Optional.ofNullable(path); - roles.roleClaimSeparator = Optional.ofNullable(sep); - return roles; - } - } - - @ConfigGroup - public static class Authentication { - - /** - * Defines a fixed list of scopes which should be added to authorization requests when authenticating users using the - * Authorization Code Grant Type. - * - */ - @ConfigItem - public List scopes; - } - - public enum ApplicationType { - /** - * A {@code WEB_APP} is a client that server pages, usually a frontend application. For this type of client the - * Authorization Code Flow is - * defined as the preferred method for authenticating users. - */ - WEB_APP, - - /** - * A {@code SERVICE} is a client that has a set of protected HTTP resources, usually a backend application following the - * RESTful Architectural Design. For this type of client, the Bearer Authorization method is defined as the preferred - * method for authenticating and authorizing users. - */ - SERVICE - } + @ConfigDocSection + @ConfigDocMapKey("tenant") + @ConfigItem(name = ConfigItem.PARENT) + public Map namedTenants; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 3acc3f79df4aa..8aaa7ac3eb6b4 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -2,14 +2,19 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.consumer.InvalidJwtException; +import io.quarkus.oidc.OIDCException; import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.SecurityIdentity; @@ -18,27 +23,13 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.ext.auth.oauth2.AccessToken; -import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.web.RoutingContext; @ApplicationScoped public class OidcIdentityProvider implements IdentityProvider { - private volatile OAuth2Auth auth; - private volatile OidcConfig config; - - public OAuth2Auth getAuth() { - return auth; - } - - public OidcIdentityProvider setAuth(OAuth2Auth auth) { - this.auth = auth; - return this; - } - - public OidcIdentityProvider setConfig(OidcConfig config) { - this.config = config; - return this; - } + @Inject + DefaultTenantConfigResolver tenantResolver; @Override public Class getRequestType() { @@ -49,41 +40,60 @@ public Class getRequestType() { @Override public CompletionStage authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) { - CompletableFuture result = new CompletableFuture<>(); - auth.decodeToken(request.getToken().getToken(), new Handler>() { + return context.runBlocking(new Supplier() { @Override - public void handle(AsyncResult event) { - if (event.failed()) { - result.completeExceptionally(new AuthenticationFailedException()); - return; - } - AccessToken token = event.result(); - QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); + public SecurityIdentity get() { + CompletableFuture result = new CompletableFuture<>(); + ContextAwareTokenCredential credential = (ContextAwareTokenCredential) request.getToken(); + RoutingContext vertxContext = credential.getContext(); + OidcTenantConfig config = tenantResolver.resolve(vertxContext).oidcConfig; - JsonWebToken jwtPrincipal; - try { - jwtPrincipal = new OidcJwtCallerPrincipal(JwtClaims.parse(token.accessToken().encode())); - } catch (InvalidJwtException e) { - result.completeExceptionally(e); - return; - } - builder.setPrincipal(jwtPrincipal); - try { - String clientId = config.getClientId().isPresent() ? config.getClientId().get() : null; - for (String role : OidcUtils.findRoles(clientId, config.getRoles(), token.accessToken())) { - builder.addRole(role); - } - } catch (Exception e) { - result.completeExceptionally(e); - return; - } + tenantResolver.resolve(vertxContext).auth.decodeToken(request.getToken().getToken(), + new Handler>() { + @Override + public void handle(AsyncResult event) { + if (event.failed()) { + result.completeExceptionally(new AuthenticationFailedException()); + return; + } + AccessToken token = event.result(); + try { + OidcUtils.validateClaims(config.getToken(), token.accessToken()); + } catch (OIDCException e) { + result.completeExceptionally(new AuthenticationFailedException(e)); + return; + } - builder.addCredential(request.getToken()); - result.complete(builder.build()); + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); + builder.addCredential(request.getToken()); + + JsonWebToken jwtPrincipal; + try { + JwtClaims jwtClaims = JwtClaims.parse(token.accessToken().encode()); + jwtClaims.setClaim(Claims.raw_token.name(), credential.getToken()); + jwtPrincipal = new OidcJwtCallerPrincipal(jwtClaims, request.getToken(), + config.token.principalClaim.isPresent() ? config.token.principalClaim.get() : null); + } catch (InvalidJwtException e) { + result.completeExceptionally(new AuthenticationFailedException(e)); + return; + } + builder.setPrincipal(jwtPrincipal); + try { + String clientId = config.getClientId().isPresent() ? config.getClientId().get() : null; + for (String role : OidcUtils.findRoles(clientId, config.getRoles(), token.accessToken())) { + builder.addRole(role); + } + } catch (Exception e) { + result.completeExceptionally(new ForbiddenException(e)); + return; + } + + result.complete(builder.build()); + } + }); + + return result.join(); } }); - - return result; } - } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java index d3aeb5ad6f28b..ea85551be223c 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java @@ -54,6 +54,10 @@ private JsonWebToken getTokenCredential(Class type) { if (identity.isAnonymous()) { return new NullJsonWebToken(); } + if (identity.getPrincipal() instanceof OidcJwtCallerPrincipal + && ((OidcJwtCallerPrincipal) identity.getPrincipal()).getCredential().getClass() == type) { + return (JsonWebToken) identity.getPrincipal(); + } TokenCredential credential = identity.getCredential(type); if (credential != null) { JwtClaims jwtClaims; @@ -66,7 +70,7 @@ private JsonWebToken getTokenCredential(Class type) { throw new RuntimeException(e); } jwtClaims.setClaim(Claims.raw_token.name(), credential.getToken()); - return new OidcJwtCallerPrincipal(jwtClaims); + return new OidcJwtCallerPrincipal(jwtClaims, credential); } throw new IllegalStateException("Current identity not associated with an access token"); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJwtCallerPrincipal.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJwtCallerPrincipal.java index 74218301408dd..632c721532433 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJwtCallerPrincipal.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJwtCallerPrincipal.java @@ -1,21 +1,46 @@ package io.quarkus.oidc.runtime; +import java.util.Optional; + import org.jose4j.jwt.JwtClaims; +import io.quarkus.security.credential.TokenCredential; import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal; /** - * An implementation of JWTCallerPrincipal that builds on the Elytron attributes + * An implementation of JWTCallerPrincipal */ public class OidcJwtCallerPrincipal extends DefaultJWTCallerPrincipal { private JwtClaims claims; + private String principalClaim; + private TokenCredential credential; + + public OidcJwtCallerPrincipal(final JwtClaims claims, TokenCredential credential) { + this(claims, credential, null); + } - public OidcJwtCallerPrincipal(final JwtClaims claims) { + public OidcJwtCallerPrincipal(final JwtClaims claims, TokenCredential credential, String principalClaim) { super(claims); this.claims = claims; + this.credential = credential; + this.principalClaim = principalClaim; } public JwtClaims getClaims() { return claims; } + + public TokenCredential getCredential() { + return credential; + } + + @Override + public String getName() { + if (principalClaim != null) { + Optional claim = super.claim(principalClaim); + return claim.orElse(null); + } else { + return super.getName(); + } + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index 538f3f07a781f..803bb7e3ca0ac 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -1,6 +1,9 @@ package io.quarkus.oidc.runtime; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import org.jboss.logging.Logger; @@ -22,34 +25,62 @@ public class OidcRecorder { private static final Logger LOG = Logger.getLogger(OidcRecorder.class); public void setup(OidcConfig config, RuntimeValue vertx, BeanContainer beanContainer) { + final Vertx vertxValue = vertx.getValue(); + Map tenantsConfig = new HashMap<>(); + + for (Map.Entry tenant : config.namedTenants.entrySet()) { + tenantsConfig.put(tenant.getKey(), createTenantContext(vertxValue, tenant.getValue())); + } + + DefaultTenantConfigResolver resolver = beanContainer.instance(DefaultTenantConfigResolver.class); + + resolver.setDefaultTenant(createTenantContext(vertxValue, config.defaultTenant)); + resolver.setTenantsConfig(tenantsConfig); + resolver.setTenantConfigContextFactory(new Function() { + @Override + public TenantConfigContext apply(OidcTenantConfig config) { + return createTenantContext(vertxValue, config); + } + }); + } + + private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oidcConfig) { OAuth2ClientOptions options = new OAuth2ClientOptions(); + if (!oidcConfig.getAuthServerUrl().isPresent()) { + return null; + } + // Base IDP server URL - options.setSite(config.authServerUrl); + options.setSite(oidcConfig.getAuthServerUrl().get()); // RFC7662 introspection service address - if (config.introspectionPath.isPresent()) { - options.setIntrospectionPath(config.introspectionPath.get()); + if (oidcConfig.getIntrospectionPath().isPresent()) { + options.setIntrospectionPath(oidcConfig.getIntrospectionPath().get()); } // RFC7662 JWKS service address - if (config.jwksPath.isPresent()) { - options.setJwkPath(config.jwksPath.get()); + if (oidcConfig.getJwksPath().isPresent()) { + options.setJwkPath(oidcConfig.getJwksPath().get()); } - if (config.clientId.isPresent()) { - options.setClientID(config.clientId.get()); + if (oidcConfig.getClientId().isPresent()) { + options.setClientID(oidcConfig.getClientId().get()); } - if (config.credentials.secret.isPresent()) { - options.setClientSecret(config.credentials.secret.get()); + if (oidcConfig.getCredentials().secret.isPresent()) { + options.setClientSecret(oidcConfig.getCredentials().secret.get()); } - if (config.publicKey.isPresent()) { + if (oidcConfig.getPublicKey().isPresent()) { options.addPubSecKey(new PubSecKeyOptions() .setAlgorithm("RS256") - .setPublicKey(config.publicKey.get())); + .setPublicKey(oidcConfig.getPublicKey().get())); + } + if (oidcConfig.getToken().issuer.isPresent()) { + options.setValidateIssuer(false); } - final long connectionDelayInSecs = config.connectionDelay.isPresent() ? config.connectionDelay.get().toMillis() / 1000 + final long connectionDelayInSecs = oidcConfig.getConnectionDelay().isPresent() + ? oidcConfig.getConnectionDelay().get().toMillis() / 1000 : 0; final long connectionRetryCount = connectionDelayInSecs > 1 ? connectionDelayInSecs / 2 : 1; if (connectionRetryCount > 1) { @@ -60,7 +91,7 @@ public void setup(OidcConfig config, RuntimeValue vertx, BeanContainer be for (long i = 0; i < connectionRetryCount; i++) { try { CompletableFuture cf = new CompletableFuture<>(); - KeycloakAuth.discover(vertx.getValue(), options, new Handler>() { + KeycloakAuth.discover(vertx, options, new Handler>() { @Override public void handle(AsyncResult event) { if (event.failed()) { @@ -87,18 +118,7 @@ public void handle(AsyncResult event) { } } - OidcIdentityProvider identityProvider = beanContainer.instance(OidcIdentityProvider.class); - identityProvider.setAuth(auth); - identityProvider.setConfig(config); - AbstractOidcAuthenticationMechanism mechanism = null; - - if (OidcConfig.ApplicationType.SERVICE.equals(config.applicationType)) { - mechanism = beanContainer.instance(BearerAuthenticationMechanism.class); - } else if (OidcConfig.ApplicationType.WEB_APP.equals(config.applicationType)) { - mechanism = beanContainer.instance(CodeAuthenticationMechanism.class); - } - - mechanism.setAuth(auth, config); + return new TenantConfigContext(auth, oidcConfig); } protected static OIDCException toOidcException(Throwable cause) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java new file mode 100644 index 0000000000000..10175ea401357 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java @@ -0,0 +1,312 @@ +package io.quarkus.oidc.runtime; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class OidcTenantConfig { + + /** + * The maximum amount of time the adapter will try connecting to the currently unavailable OIDC server for. + * For example, setting it to '20S' will let the adapter keep requesting the connection for up to 20 seconds. + */ + @ConfigItem + public Optional connectionDelay = Optional.empty();; + + /** + * The base URL of the OpenID Connect (OIDC) server, for example, 'https://host:port/auth'. + * All the other OIDC server page and service URLs are derived from this URL. + * Note if you work with Keycloak OIDC server, make sure the base URL is in the following format: + * 'https://host:port/auth/realms/{realm}' where '{realm}' has to be replaced by the name of the Keycloak realm. + */ + @ConfigItem + Optional authServerUrl = Optional.empty(); + /** + * Relative path of the RFC7662 introspection service. + */ + @ConfigItem + Optional introspectionPath = Optional.empty(); + /** + * Relative path of the OIDC service returning a JWK set. + */ + @ConfigItem + Optional jwksPath = Optional.empty(); + /** + * Public key for the local JWT token verification. + */ + @ConfigItem + Optional publicKey = Optional.empty(); + /** + * The client-id of the application. Each application has a client-id that is used to identify the application + */ + @ConfigItem + Optional clientId = Optional.empty(); + /** + * Configuration to find and parse a custom claim containing the roles information. + */ + @ConfigItem + Roles roles = new Roles(); + /** + * Configuration how to validate the token claims. + */ + @ConfigItem + Token token = new Token(); + /** + * Credentials which the OIDC adapter will use to authenticate to the OIDC server. + */ + @ConfigItem + Credentials credentials = new Credentials(); + /** + * Different options to configure authorization requests + */ + Authentication authentication = new Authentication(); + + public Optional getConnectionDelay() { + return connectionDelay; + } + + public void setConnectionDelay(Duration connectionDelay) { + this.connectionDelay = Optional.of(connectionDelay); + } + + public Optional getAuthServerUrl() { + return authServerUrl; + } + + public void setAuthServerUrl(String authServerUrl) { + this.authServerUrl = Optional.of(authServerUrl); + } + + public Optional getIntrospectionPath() { + return introspectionPath; + } + + public void setIntrospectionPath(String introspectionPath) { + this.introspectionPath = Optional.of(introspectionPath); + } + + public Optional getJwksPath() { + return jwksPath; + } + + public void setJwksPath(String jwksPath) { + this.jwksPath = Optional.of(jwksPath); + } + + public Optional getPublicKey() { + return publicKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = Optional.of(publicKey); + } + + public Optional getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = Optional.of(clientId); + } + + public Roles getRoles() { + return roles; + } + + public void setRoles(Roles roles) { + this.roles = roles; + } + + public Token getToken() { + return token; + } + + public void setToken(Token token) { + this.token = token; + } + + public Credentials getCredentials() { + return credentials; + } + + public void setCredentials(Credentials credentials) { + this.credentials = credentials; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + + @ConfigGroup + public static class Credentials { + + /** + * The client secret + */ + @ConfigItem + Optional secret = Optional.empty(); + + public Optional getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = Optional.of(secret); + } + } + + @ConfigGroup + public static class Roles { + + public static Roles fromClaimPath(String path) { + return fromClaimPathAndSeparator(path, null); + } + + public static Roles fromClaimPathAndSeparator(String path, String sep) { + Roles roles = new Roles(); + roles.roleClaimPath = Optional.ofNullable(path); + roles.roleClaimSeparator = Optional.ofNullable(sep); + return roles; + } + + /** + * Path to the claim containing an array of groups. It starts from the top level JWT JSON object and + * can contain multiple segments where each segment represents a JSON object name only, example: "realm/groups". + * This property can be used if a token has no 'groups' claim but has the groups set in a different claim. + */ + @ConfigItem + Optional roleClaimPath = Optional.empty(); + /** + * Separator for splitting a string which may contain multiple group values. + * It will only be used if the "role-claim-path" property points to a custom claim whose value is a string. + * A single space will be used by default because the standard 'scope' claim may contain a space separated sequence. + */ + @ConfigItem + Optional roleClaimSeparator = Optional.empty(); + + public Optional getRoleClaimPath() { + return roleClaimPath; + } + + public void setRoleClaimPath(String roleClaimPath) { + this.roleClaimPath = Optional.of(roleClaimPath); + } + + public Optional getRoleClaimSeparator() { + return roleClaimSeparator; + } + + public void setRoleClaimSeparator(String roleClaimSeparator) { + this.roleClaimSeparator = Optional.of(roleClaimSeparator); + } + } + + /** + * Defines the authorization request properties when authenticating + * users using the Authorization Code Grant Type. + */ + @ConfigGroup + public static class Authentication { + /** + * Relative path for calculating a "redirect_uri" parameter. + * It set it will be appended to the request URI's host and port, otherwise the complete request URI will be used. + * It has to start from the forward slash, for example: "/service" + * + */ + @ConfigItem + public Optional redirectPath = Optional.empty(); + + /** + * List of scopes + * + */ + @ConfigItem + public Optional> scopes = Optional.empty(); + + public Optional getRedirectPath() { + return redirectPath; + } + + public void setRedirectPath(String redirectPath) { + this.redirectPath = Optional.of(redirectPath); + } + + public Optional> getScopes() { + return scopes; + } + + public void setScopes(Optional> scopes) { + this.scopes = scopes; + } + } + + @ConfigGroup + public static class Token { + + public static Token fromIssuer(String issuer) { + Token tokenClaims = new Token(); + tokenClaims.issuer = Optional.of(issuer); + tokenClaims.audience = Optional.ofNullable(null); + return tokenClaims; + } + + public static Token fromAudience(String... audience) { + Token tokenClaims = new Token(); + tokenClaims.issuer = Optional.ofNullable(null); + tokenClaims.audience = Optional.of(Arrays.asList(audience)); + return tokenClaims; + } + + /** + * Expected issuer 'iss' claim value + */ + @ConfigItem + public Optional issuer = Optional.empty(); + + /** + * Expected audience `aud` claim value which may be a string or an array of strings + */ + @ConfigItem + public Optional> audience = Optional.empty(); + + /** + * Name of the claim which contains a principal name. By default, the 'upn', 'preferred_username' and `sub` claims are + * checked. + */ + @ConfigItem + public Optional principalClaim = Optional.empty(); + + public Optional getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = Optional.of(issuer); + } + + public Optional> getAudience() { + return audience; + } + + public void setAudience(List audience) { + this.audience = Optional.of(audience); + } + + public Optional getPrincipalClaim() { + return principalClaim; + } + + public void setPrincipalClaim(String principalClaim) { + this.principalClaim = Optional.of(principalClaim); + } + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 450549d7cd066..b6722f655dcbc 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -1,30 +1,55 @@ package io.quarkus.oidc.runtime; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.stream.Collectors; + +import org.eclipse.microprofile.jwt.Claims; import io.quarkus.oidc.OIDCException; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; public final class OidcUtils { + private OidcUtils() { } - public static List findRoles(String clientId, OidcConfig.Roles rolesConfig, JsonObject json) throws Exception { + public static boolean validateClaims(OidcTenantConfig.Token tokenConfig, JsonObject json) { + if (tokenConfig.issuer.isPresent()) { + String issuer = json.getString(Claims.iss.name()); + if (!tokenConfig.issuer.get().equals(issuer)) { + throw new OIDCException("Invalid issuer"); + } + } + if (tokenConfig.audience.isPresent()) { + Object claimValue = json.getValue(Claims.aud.name()); + List audience = Collections.emptyList(); + if (claimValue instanceof JsonArray) { + audience = convertJsonArrayToList((JsonArray) claimValue); + } else if (claimValue != null) { + audience = Arrays.asList((String) claimValue); + } + if (!audience.containsAll(tokenConfig.audience.get())) { + throw new OIDCException("Invalid audience"); + } + } + return true; + } + + public static List findRoles(String clientId, OidcTenantConfig.Roles rolesConfig, JsonObject json) { // If the user configured a specific path - check and enforce a claim at this path exists if (rolesConfig.getRoleClaimPath().isPresent()) { return findClaimWithRoles(rolesConfig, rolesConfig.getRoleClaimPath().get(), json, true); } // Check 'groups' next - List groups = findClaimWithRoles(rolesConfig, "groups", json, false); + List groups = findClaimWithRoles(rolesConfig, Claims.groups.name(), json, false); if (!groups.isEmpty()) { - return groups.stream().map(v -> v.toString()).collect(Collectors.toList()); + return groups; } else { // Finally, check if this token has been issued by Keycloak. // Return an empty or populated list of realm and resource access roles @@ -39,12 +64,12 @@ public static List findRoles(String clientId, OidcConfig.Roles rolesConf } - private static List findClaimWithRoles(OidcConfig.Roles rolesConfig, String claimPath, + private static List findClaimWithRoles(OidcTenantConfig.Roles rolesConfig, String claimPath, JsonObject json, boolean mustExist) { Object claimValue = findClaimValue(claimPath, json, claimPath.split("/"), 0, mustExist); if (claimValue instanceof JsonArray) { - return ((JsonArray) claimValue).stream().map(v -> v.toString()).collect(Collectors.toList()); + return convertJsonArrayToList((JsonArray) claimValue); } else if (claimValue != null) { String sep = rolesConfig.getRoleClaimSeparator().isPresent() ? rolesConfig.getRoleClaimSeparator().get() : " "; return Arrays.asList(claimValue.toString().split(sep)); @@ -57,8 +82,7 @@ private static Object findClaimValue(String claimPath, JsonObject json, String[] Object claimValue = json.getValue(pathArray[step]); if (claimValue == null) { if (mustExist) { - throw new OIDCException( - "No claim exists at the path " + claimPath + " at the path segment " + pathArray[step]); + throw new OIDCException("No claim exists at the path " + claimPath + " at the path segment " + pathArray[step]); } } else if (step + 1 < pathArray.length) { if (claimValue instanceof JsonObject) { @@ -71,4 +95,12 @@ private static Object findClaimValue(String claimPath, JsonObject json, String[] return claimValue; } + + private static List convertJsonArrayToList(JsonArray claimValue) { + List list = new ArrayList<>(claimValue.size()); + for (int i = 0; i < claimValue.size(); i++) { + list.add(claimValue.getString(i)); + } + return list; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java new file mode 100644 index 0000000000000..97e319c985e06 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java @@ -0,0 +1,15 @@ +package io.quarkus.oidc.runtime; + +import io.vertx.ext.auth.oauth2.OAuth2Auth; + +class TenantConfigContext { + + OAuth2Auth auth; + OidcTenantConfig oidcConfig; + + TenantConfigContext(OAuth2Auth auth, OidcTenantConfig config) { + this.auth = auth; + oidcConfig = config; + } + +} diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java index 39cd4e59d39b7..3750c4ab24bc9 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java @@ -14,13 +14,72 @@ import org.junit.jupiter.api.Test; +import io.quarkus.oidc.OIDCException; import io.vertx.core.json.JsonObject; public class OidcUtilsTest { + @Test + public void testTokenWithCorrectIssuer() throws Exception { + OidcTenantConfig.Token tokenClaims = OidcTenantConfig.Token.fromIssuer("https://server.example.com"); + InputStream is = getClass().getResourceAsStream("/tokenIssuer.json"); + assertTrue(OidcUtils.validateClaims(tokenClaims, read(is))); + } + + @Test + public void testTokenWithWrongIssuer() throws Exception { + OidcTenantConfig.Token tokenClaims = OidcTenantConfig.Token.fromIssuer("https://servers.example.com"); + InputStream is = getClass().getResourceAsStream("/tokenIssuer.json"); + try { + OidcUtils.validateClaims(tokenClaims, read(is)); + fail("Exception expected: wrong issuer"); + } catch (OIDCException ex) { + // expected + } + } + + @Test + public void testTokenWithCorrectStringAudience() throws Exception { + OidcTenantConfig.Token tokenClaims = OidcTenantConfig.Token.fromAudience("https://quarkus.example.com"); + InputStream is = getClass().getResourceAsStream("/tokenStringAudience.json"); + assertTrue(OidcUtils.validateClaims(tokenClaims, read(is))); + } + + @Test + public void testTokenWithWrongStringAudience() throws Exception { + OidcTenantConfig.Token tokenClaims = OidcTenantConfig.Token.fromIssuer("https://quarkus.examples.com"); + InputStream is = getClass().getResourceAsStream("/tokenStringAudience.json"); + try { + OidcUtils.validateClaims(tokenClaims, read(is)); + fail("Exception expected: wrong audience"); + } catch (OIDCException ex) { + // expected + } + } + + @Test + public void testTokenWithCorrectArrayAudience() throws Exception { + OidcTenantConfig.Token tokenClaims = OidcTenantConfig.Token.fromAudience("https://quarkus.example.com", + "frontend_client_id"); + InputStream is = getClass().getResourceAsStream("/tokenArrayAudience.json"); + assertTrue(OidcUtils.validateClaims(tokenClaims, read(is))); + } + + @Test + public void testTokenWithWrongArrayAudience() throws Exception { + OidcTenantConfig.Token tokenClaims = OidcTenantConfig.Token.fromAudience("service_client_id"); + InputStream is = getClass().getResourceAsStream("/tokenArrayAudience.json"); + try { + OidcUtils.validateClaims(tokenClaims, read(is)); + fail("Exception expected: wrong array audience"); + } catch (OIDCException ex) { + // expected + } + } + @Test public void testKeycloakRealmAccessToken() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath(null); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath(null); List roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenKeycloakRealmAccess.json"))); assertEquals(2, roles.size()); @@ -30,7 +89,7 @@ public void testKeycloakRealmAccessToken() throws Exception { @Test public void testKeycloakRealmAndResourceAccessTokenClient1() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath(null); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath(null); List roles = OidcUtils.findRoles("client1", rolesCfg, read(getClass().getResourceAsStream("/tokenKeycloakResourceAccess.json"))); assertEquals(2, roles.size()); @@ -40,7 +99,7 @@ public void testKeycloakRealmAndResourceAccessTokenClient1() throws Exception { @Test public void testKeycloakRealmAndResourceAccessTokenClient2() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath(null); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath(null); List roles = OidcUtils.findRoles("client2", rolesCfg, read(getClass().getResourceAsStream("/tokenKeycloakResourceAccess.json"))); assertEquals(2, roles.size()); @@ -50,7 +109,7 @@ public void testKeycloakRealmAndResourceAccessTokenClient2() throws Exception { @Test public void testKeycloakRealmAndResourceAccessTokenNullClient() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath(null); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath(null); List roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenKeycloakResourceAccess.json"))); assertEquals(1, roles.size()); @@ -59,7 +118,7 @@ public void testKeycloakRealmAndResourceAccessTokenNullClient() throws Exception @Test public void testTokenWithGroups() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath(null); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath(null); List roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenGroups.json"))); assertEquals(2, roles.size()); assertTrue(roles.contains("group1")); @@ -68,7 +127,7 @@ public void testTokenWithGroups() throws Exception { @Test public void testTokenWithCustomRoles() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath("application_card/embedded/roles"); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath("application_card/embedded/roles"); List roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenCustomPath.json"))); assertEquals(2, roles.size()); assertTrue(roles.contains("r1")); @@ -77,7 +136,7 @@ public void testTokenWithCustomRoles() throws Exception { @Test public void testTokenWithScope() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath("scope"); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath("scope"); List roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenScope.json"))); assertEquals(2, roles.size()); assertTrue(roles.contains("s1")); @@ -86,7 +145,7 @@ public void testTokenWithScope() throws Exception { @Test public void testTokenWithCustomScope() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPathAndSeparator("customScope", ","); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPathAndSeparator("customScope", ","); List roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenCustomScope.json"))); assertEquals(2, roles.size()); @@ -96,7 +155,7 @@ public void testTokenWithCustomScope() throws Exception { @Test public void testTokenWithCustomRolesWrongPath() throws Exception { - OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath("application-card/embedded/roles"); + OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath("application-card/embedded/roles"); InputStream is = getClass().getResourceAsStream("/tokenCustomPath.json"); try { OidcUtils.findRoles(null, rolesCfg, read(is)); diff --git a/extensions/oidc/runtime/src/test/resources/tokenArrayAudience.json b/extensions/oidc/runtime/src/test/resources/tokenArrayAudience.json new file mode 100644 index 0000000000000..4b67b6c123ad8 --- /dev/null +++ b/extensions/oidc/runtime/src/test/resources/tokenArrayAudience.json @@ -0,0 +1,11 @@ +{ + "iss": "https://server.example.com", + "aud": ["https://quarkus.example.com", "frontend_client_id"], + "jti": "a-123", + "sub": "24400320", + "upn": "jdoe@example.com", + "preferred_username": "jdoe", + "exp": 1311281970, + "iat": 1311280970, + "auth_time": 1311280969 +} diff --git a/extensions/oidc/runtime/src/test/resources/tokenIssuer.json b/extensions/oidc/runtime/src/test/resources/tokenIssuer.json new file mode 100644 index 0000000000000..4e23506295093 --- /dev/null +++ b/extensions/oidc/runtime/src/test/resources/tokenIssuer.json @@ -0,0 +1,11 @@ +{ + "iss": "https://server.example.com", + "jti": "a-123", + "sub": "24400320", + "upn": "jdoe@example.com", + "preferred_username": "jdoe", + "aud": "s6BhdRkqt3", + "exp": 1311281970, + "iat": 1311280970, + "auth_time": 1311280969 +} diff --git a/extensions/oidc/runtime/src/test/resources/tokenStringAudience.json b/extensions/oidc/runtime/src/test/resources/tokenStringAudience.json new file mode 100644 index 0000000000000..5fc5ce7b6257b --- /dev/null +++ b/extensions/oidc/runtime/src/test/resources/tokenStringAudience.json @@ -0,0 +1,11 @@ +{ + "iss": "https://server.example.com", + "aud": "https://quarkus.example.com", + "jti": "a-123", + "sub": "24400320", + "upn": "jdoe@example.com", + "preferred_username": "jdoe", + "exp": 1311281970, + "iat": 1311280970, + "auth_time": 1311280969 +} diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java index bbfb8146778fd..82ce06315c2b5 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java @@ -30,6 +30,7 @@ import io.quarkus.panache.common.deployment.EntityModel; import io.quarkus.panache.common.deployment.MetamodelInfo; import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer; +import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; public final class PanacheResourceProcessor { @@ -82,9 +83,13 @@ void build(CombinedIndexBuildItem index, // Skip PanacheRepository if (classInfo.name().equals(DOTNAME_PANACHE_REPOSITORY)) continue; + if (PanacheRepositoryEnhancer.skipRepository(classInfo)) + continue; daoClasses.add(classInfo.name().toString()); } for (ClassInfo classInfo : index.getIndex().getAllKnownImplementors(DOTNAME_PANACHE_REPOSITORY)) { + if (PanacheRepositoryEnhancer.skipRepository(classInfo)) + continue; daoClasses.add(classInfo.name().toString()); } for (String daoClass : daoClasses) { @@ -95,6 +100,7 @@ void build(CombinedIndexBuildItem index, Set modelClasses = new HashSet<>(); // Note that we do this in two passes because for some reason Jandex does not give us subtypes // of PanacheEntity if we ask for subtypes of PanacheEntityBase + // NOTE: we don't skip abstract/generic entities because they still need accessors for (ClassInfo classInfo : index.getIndex().getAllKnownSubclasses(DOTNAME_PANACHE_ENTITY_BASE)) { // FIXME: should we really skip PanacheEntity or all MappedSuperClass? if (classInfo.name().equals(DOTNAME_PANACHE_ENTITY)) @@ -121,5 +127,4 @@ void build(CombinedIndexBuildItem index, } } } - } diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheEntityBase.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheEntityBase.java index a4b97ebc6e9ba..362efb9aaa921 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheEntityBase.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheEntityBase.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import javax.json.bind.annotation.JsonbTransient; @@ -115,6 +116,29 @@ public static T findById(Object id, LockModeType l throw JpaOperations.implementationInjectionMissing(); } + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @return if found, an optional containing the entity, else Optional.empty(). + */ + @GenerateBridge + public static Optional findByIdOptional(Object id) { + throw JpaOperations.implementationInjectionMissing(); + } + + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @param lockModeType the locking strategy to be used when retrieving the entity. + * @return if found, an optional containing the entity, else Optional.empty(). + */ + @GenerateBridge + public static Optional findByIdOptional(Object id, LockModeType lockModeType) { + throw JpaOperations.implementationInjectionMissing(); + } + /** * Find entities using a query, with optional indexed parameters. * @@ -694,4 +718,47 @@ public static void persist(Stream entities) { public static void persist(Object firstEntity, Object... entities) { JpaOperations.persist(firstEntity, entities); } + + /** + * Update all entities of this type matching the given query, with mandatory indexed parameters. + * + * @param query a {@link io.quarkus.hibernate.orm.panache query string} + * @param params optional sequence of indexed parameters + * @return the number of entities updated. + * @see #update(String, Map) + * @see #update(String, Parameters) + */ + @GenerateBridge + public static int update(String query, Object... params) { + throw JpaOperations.implementationInjectionMissing(); + } + + /** + * Update all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.hibernate.orm.panache query string} + * @param params {@link Map} of named parameters + * @return the number of entities updated. + * @see #update(String, Object...) + * @see #update(String, Parameters) + * + */ + @GenerateBridge + public static int update(String query, Map params) { + throw JpaOperations.implementationInjectionMissing(); + } + + /** + * Update all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.hibernate.orm.panache query string} + * @param params {@link Parameters} of named parameters + * @return the number of entities updated. + * @see #update(String, Object...) + * @see #update(String, Map) + */ + @GenerateBridge + public static int update(String query, Parameters params) { + throw JpaOperations.implementationInjectionMissing(); + } } diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java index a0935fe22c9d1..81be409f43994 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java @@ -1,6 +1,7 @@ package io.quarkus.hibernate.orm.panache; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; import javax.persistence.LockModeType; @@ -123,6 +124,15 @@ public interface PanacheQuery { */ public PanacheQuery withLock(LockModeType lockModeType); + /** + * Set a query property or hint on the underlying JPA Query. + * + * @param hintName name of the property or hint. + * @param value value for the property or hint. + * @return this query, modified + */ + public PanacheQuery withHint(String hintName, Object value); + // Results /** @@ -163,6 +173,15 @@ public interface PanacheQuery { */ public T firstResult(); + /** + * Returns the first result of the current page index. This ignores the current page size to fetch + * a single result. + * + * @return if found, an optional containing the entity, else Optional.empty(). + * @see #singleResultOptional() + */ + public Optional firstResultOptional(); + /** * Executes this query for the current page and return a single result. * @@ -172,4 +191,13 @@ public interface PanacheQuery { * @see #firstResult() */ public T singleResult(); + + /** + * Executes this query for the current page and return a single result. + * + * @return if found, an optional containing the entity, else Optional.empty(). + * @throws NonUniqueResultException if there are more than one result + * @see #firstResultOptional() + */ + public Optional singleResultOptional(); } diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheRepositoryBase.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheRepositoryBase.java index e40a45094e8be..2b474e29ac1e8 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheRepositoryBase.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheRepositoryBase.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import javax.persistence.LockModeType; @@ -113,6 +114,28 @@ public default Entity findById(Id id, LockModeType lockModeType) { throw JpaOperations.implementationInjectionMissing(); } + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @return if found, an optional containing the entity, else Optional.empty(). + */ + @GenerateBridge + public default Optional findByIdOptional(Id id) { + throw JpaOperations.implementationInjectionMissing(); + } + + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @return if found, an optional containing the entity, else Optional.empty(). + */ + @GenerateBridge + public default Optional findByIdOptional(Id id, LockModeType lockModeType) { + throw JpaOperations.implementationInjectionMissing(); + } + /** * Find entities using a query, with optional indexed parameters. * @@ -691,4 +714,46 @@ public default void persist(Stream entities) { public default void persist(Entity firstEntity, @SuppressWarnings("unchecked") Entity... entities) { JpaOperations.persist(firstEntity, entities); } + + /** + * Update all entities of this type matching the given query, with optional indexed parameters. + * + * @param query a {@link io.quarkus.hibernate.orm.panache query string} + * @param params optional sequence of indexed parameters + * @return the number of entities updated. + * @see #update(String, Map) + * @see #update(String, Parameters) + */ + @GenerateBridge + public default int update(String query, Object... params) { + throw JpaOperations.implementationInjectionMissing(); + } + + /** + * Update all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.hibernate.orm.panache query string} + * @param params {@link Map} of named parameters + * @return the number of entities updated. + * @see #update(String, Object...) + * @see #update(String, Parameters) + */ + @GenerateBridge + public default int update(String query, Map params) { + throw JpaOperations.implementationInjectionMissing(); + } + + /** + * Update all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.hibernate.orm.panache query string} + * @param params {@link Parameters} of named parameters + * @return the number of entities updated. + * @see #update(String, Object...) + * @see #update(String, Map) + */ + @GenerateBridge + public default int update(String query, Parameters params) { + throw JpaOperations.implementationInjectionMissing(); + } } diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/package-info.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/package-info.java index 0aa4c33581b4f..b71b24af657d5 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/package-info.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/package-info.java @@ -33,7 +33,7 @@ * at the end. *

*

- * If your query does not start with from, we support the following additional forms: + * If your select query does not start with from, we support the following additional forms: *

*
    *
  • order by ... which will expand to from EntityName order by ...
  • @@ -42,6 +42,16 @@ *
  • <query> will expand to from EntityName where <query>
  • *
* + * If your update query does not start with update from, we support the following additional forms: + *

+ *
    + *
  • from EntityName ... which will expand to update from EntityName ...
  • + *
  • set? <singleColumnName> (and single parameter) which will expand to + * update from EntityName set <singleColumnName> = ?
  • + *
  • set? <update-query> will expand to + * update from EntityName set <update-query> = ?
  • + *
+ * * @author Stéphane Épardaud */ package io.quarkus.hibernate.orm.panache; diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/AdditionalJpaOperations.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/AdditionalJpaOperations.java new file mode 100644 index 0000000000000..cc267002b261e --- /dev/null +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/AdditionalJpaOperations.java @@ -0,0 +1,40 @@ +package io.quarkus.hibernate.orm.panache.runtime; + +import java.util.Map; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; + +//TODO this class is only needed by the Spring Data JPA module and would be placed there it it weren't for a dev-mode classloader issue +// see https://github.com/quarkusio/quarkus/issues/6214 +public class AdditionalJpaOperations { + + @SuppressWarnings("rawtypes") + public static PanacheQuery find(Class entityClass, String query, String countQuery, Sort sort, + Map params) { + String findQuery = JpaOperations.createFindQuery(entityClass, query, JpaOperations.paramCount(params)); + EntityManager em = JpaOperations.getEntityManager(); + Query jpaQuery = em.createQuery(sort != null ? findQuery + JpaOperations.toOrderBy(sort) : findQuery); + JpaOperations.bindParameters(jpaQuery, params); + return new CustomCountPanacheQuery(em, jpaQuery, findQuery, countQuery, params); + } + + @SuppressWarnings("rawtypes") + public static PanacheQuery find(Class entityClass, String query, String countQuery, Sort sort, + Parameters parameters) { + return find(entityClass, query, countQuery, sort, parameters.map()); + } + + @SuppressWarnings("rawtypes") + public static PanacheQuery find(Class entityClass, String query, String countQuery, Sort sort, Object... params) { + String findQuery = JpaOperations.createFindQuery(entityClass, query, JpaOperations.paramCount(params)); + EntityManager em = JpaOperations.getEntityManager(); + Query jpaQuery = em.createQuery(sort != null ? findQuery + JpaOperations.toOrderBy(sort) : findQuery); + JpaOperations.bindParameters(jpaQuery, params); + return new CustomCountPanacheQuery(em, jpaQuery, findQuery, countQuery, params); + } +} diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/CustomCountPanacheQuery.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/CustomCountPanacheQuery.java new file mode 100644 index 0000000000000..dbe86a374716c --- /dev/null +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/CustomCountPanacheQuery.java @@ -0,0 +1,22 @@ +package io.quarkus.hibernate.orm.panache.runtime; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +//TODO this class is only needed by the Spring Data JPA module and would be placed there it it weren't for a dev-mode classloader issue +// see https://github.com/quarkusio/quarkus/issues/6214 +public class CustomCountPanacheQuery extends PanacheQueryImpl { + + private final String customCountQuery; + + public CustomCountPanacheQuery(EntityManager em, Query jpaQuery, String query, String customCountQuery, + Object paramsArrayOrMap) { + super(em, jpaQuery, query, paramsArrayOrMap); + this.customCountQuery = customCountQuery; + } + + @Override + protected String countQuery() { + return customCountQuery; + } +} diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/JpaOperations.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/JpaOperations.java index 417f620f0f294..d206827936a34 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/JpaOperations.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/JpaOperations.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.stream.Stream; import javax.persistence.EntityManager; @@ -16,6 +17,7 @@ import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import io.quarkus.panache.common.Sort; +import io.quarkus.panache.common.exception.PanacheQueryException; public class JpaOperations { @@ -99,11 +101,11 @@ public static Query bindParameters(Query query, Map params) { return query; } - private static int paramCount(Object[] params) { + static int paramCount(Object[] params) { return params != null ? params.length : 0; } - private static int paramCount(Map params) { + static int paramCount(Map params) { return params != null ? params.size() : 0; } @@ -112,7 +114,7 @@ private static String getEntityName(Class entityClass) { return entityClass.getName(); } - private static String createFindQuery(Class entityClass, String query, int paramCount) { + static String createFindQuery(Class entityClass, String query, int paramCount) { if (query == null) return "FROM " + getEntityName(entityClass); @@ -155,6 +157,32 @@ private static String createCountQuery(Class entityClass, String query, int p return "SELECT COUNT(*) FROM " + getEntityName(entityClass) + " WHERE " + query; } + private static String createUpdateQuery(Class entityClass, String query, int paramCount) { + if (query == null) { + throw new PanacheQueryException("Query string cannot be null"); + } + + String trimmed = query.trim(); + if (trimmed.isEmpty()) { + throw new PanacheQueryException("Query string cannot be empty"); + } + + String trimmedLc = trimmed.toLowerCase(); + if (trimmedLc.startsWith("update ")) { + return query; + } + if (trimmedLc.startsWith("from ")) { + return "UPDATE " + query; + } + if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1 && paramCount == 1) { + query += " = ?1"; + } + if (trimmedLc.startsWith("set ")) { + return "UPDATE FROM " + getEntityName(entityClass) + " " + query; + } + return "UPDATE FROM " + getEntityName(entityClass) + " SET " + query; + } + private static String createDeleteQuery(Class entityClass, String query, int paramCount) { if (query == null) return "DELETE FROM " + getEntityName(entityClass); @@ -201,6 +229,14 @@ public static Object findById(Class entityClass, Object id, LockModeType lock return getEntityManager().find(entityClass, id, lockModeType); } + public static Optional findByIdOptional(Class entityClass, Object id) { + return Optional.ofNullable(findById(entityClass, id)); + } + + public static Optional findByIdOptional(Class entityClass, Object id, LockModeType lockModeType) { + return Optional.ofNullable(findById(entityClass, id, lockModeType)); + } + public static PanacheQuery find(Class entityClass, String query, Object... params) { return find(entityClass, query, null, params); } @@ -385,6 +421,28 @@ public static int executeUpdate(String query, Map params) { return jpaQuery.executeUpdate(); } + public static int executeUpdate(Class entityClass, String query, Object... params) { + String updateQuery = createUpdateQuery(entityClass, query, paramCount(params)); + return executeUpdate(updateQuery, params); + } + + public static int executeUpdate(Class entityClass, String query, Map params) { + String updateQuery = createUpdateQuery(entityClass, query, paramCount(params)); + return executeUpdate(updateQuery, params); + } + + public static int update(Class entityClass, String query, Map params) { + return executeUpdate(entityClass, query, params); + } + + public static int update(Class entityClass, String query, Parameters params) { + return update(entityClass, query, params.map()); + } + + public static int update(Class entityClass, String query, Object... params) { + return executeUpdate(entityClass, query, params); + } + public static void setRollbackOnly() { try { getTransactionManager().setRollbackOnly(); diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java index d21ccec9d98b1..5c5be466669d4 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java @@ -2,10 +2,12 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import javax.persistence.EntityManager; import javax.persistence.LockModeType; +import javax.persistence.NonUniqueResultException; import javax.persistence.Query; import io.quarkus.hibernate.orm.panache.PanacheQuery; @@ -97,6 +99,12 @@ public PanacheQuery withLock(LockModeType lockModeType) { return (PanacheQuery) this; } + @Override + public PanacheQuery withHint(String hintName, Object value) { + jpaQuery.setHint(hintName, value); + return (PanacheQuery) this; + } + // Results @Override @@ -108,7 +116,7 @@ public long count() { int orderByIndex = lcQuery.lastIndexOf(" order by "); if (orderByIndex != -1) query = query.substring(0, orderByIndex); - Query countQuery = em.createQuery("SELECT COUNT(*) " + query); + Query countQuery = em.createQuery(countQuery()); if (paramsArrayOrMap instanceof Map) JpaOperations.bindParameters(countQuery, (Map) paramsArrayOrMap); else @@ -118,6 +126,10 @@ public long count() { return count; } + protected String countQuery() { + return "SELECT COUNT(*) " + query; + } + @Override @SuppressWarnings("unchecked") public List list() { @@ -139,10 +151,27 @@ public T firstResult() { return list.isEmpty() ? null : list.get(0); } + @Override + public Optional firstResultOptional() { + return Optional.ofNullable(firstResult()); + } + @Override @SuppressWarnings("unchecked") public T singleResult() { jpaQuery.setMaxResults(page.size); return (T) jpaQuery.getSingleResult(); } + + @Override + @SuppressWarnings("unchecked") + public Optional singleResultOptional() { + jpaQuery.setMaxResults(2); + List list = jpaQuery.getResultList(); + if (list.size() == 2) { + throw new NonUniqueResultException(); + } + + return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)); + } } diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java index d7806dbfd25dc..6254a55737dad 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java @@ -1,13 +1,19 @@ package io.quarkus.mongodb.panache.deployment; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import org.bson.codecs.pojo.annotations.BsonProperty; import org.bson.types.ObjectId; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.Indexer; +import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; import io.quarkus.deployment.Capabilities; @@ -27,7 +33,9 @@ import io.quarkus.mongodb.panache.PanacheMongoEntityBase; import io.quarkus.mongodb.panache.PanacheMongoRepository; import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.mongodb.panache.ProjectionFor; import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer; +import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; public class PanacheResourceProcessor { static final DotName DOTNAME_PANACHE_REPOSITORY_BASE = DotName.createSimple(PanacheMongoRepositoryBase.class.getName()); @@ -35,8 +43,13 @@ public class PanacheResourceProcessor { static final DotName DOTNAME_PANACHE_ENTITY_BASE = DotName.createSimple(PanacheMongoEntityBase.class.getName()); private static final DotName DOTNAME_PANACHE_ENTITY = DotName.createSimple(PanacheMongoEntity.class.getName()); + private static final DotName DOTNAME_PROJECTION_FOR = DotName.createSimple(ProjectionFor.class.getName()); + private static final DotName DOTNAME_BSON_PROPERTY = DotName.createSimple(BsonProperty.class.getName()); + private static final DotName DOTNAME_OBJECT_ID = DotName.createSimple(ObjectId.class.getName()); + private static final DotName DOTNAME_OBJECT = DotName.createSimple(Object.class.getName()); + @BuildStep CapabilityBuildItem capability() { return new CapabilityBuildItem(Capabilities.MONGODB_PANACHE); @@ -88,9 +101,13 @@ void build(CombinedIndexBuildItem index, // Skip PanacheRepository if (classInfo.name().equals(DOTNAME_PANACHE_REPOSITORY)) continue; + if (PanacheRepositoryEnhancer.skipRepository(classInfo)) + continue; daoClasses.add(classInfo.name().toString()); } for (ClassInfo classInfo : index.getIndex().getAllKnownImplementors(DOTNAME_PANACHE_REPOSITORY)) { + if (PanacheRepositoryEnhancer.skipRepository(classInfo)) + continue; daoClasses.add(classInfo.name().toString()); } for (String daoClass : daoClasses) { @@ -125,5 +142,47 @@ void build(CombinedIndexBuildItem index, } } } + + // manage @BsonProperty for the @ProjectionFor annotation + Map> propertyMapping = new HashMap<>(); + for (AnnotationInstance annotationInstance : index.getIndex().getAnnotations(DOTNAME_PROJECTION_FOR)) { + Type targetClass = annotationInstance.value().asClass(); + ClassInfo target = index.getIndex().getClassByName(targetClass.name()); + Map classPropertyMapping = new HashMap<>(); + extractMappings(classPropertyMapping, target, index); + propertyMapping.put(target, classPropertyMapping); + } + for (AnnotationInstance annotationInstance : index.getIndex().getAnnotations(DOTNAME_PROJECTION_FOR)) { + Type targetClass = annotationInstance.value().asClass(); + ClassInfo target = index.getIndex().getClassByName(targetClass.name()); + Map targetPropertyMapping = propertyMapping.get(target); + if (targetPropertyMapping != null && !targetPropertyMapping.isEmpty()) { + ClassInfo info = annotationInstance.target().asClass(); + ProjectionForEnhancer fieldEnhancer = new ProjectionForEnhancer(targetPropertyMapping); + transformers.produce(new BytecodeTransformerBuildItem(info.name().toString(), fieldEnhancer)); + } + } + } + + private void extractMappings(Map classPropertyMapping, ClassInfo target, CombinedIndexBuildItem index) { + for (FieldInfo fieldInfo : target.fields()) { + if (fieldInfo.hasAnnotation(DOTNAME_BSON_PROPERTY)) { + AnnotationInstance bsonProperty = fieldInfo.annotation(DOTNAME_BSON_PROPERTY); + classPropertyMapping.put(fieldInfo.name(), bsonProperty.value().asString()); + } + } + for (MethodInfo methodInfo : target.methods()) { + if (methodInfo.hasAnnotation(DOTNAME_BSON_PROPERTY)) { + AnnotationInstance bsonProperty = methodInfo.annotation(DOTNAME_BSON_PROPERTY); + classPropertyMapping.put(methodInfo.name(), bsonProperty.value().asString()); + } + } + + // climb up the hierarchy of types + if (!target.superClassType().name().equals(DOTNAME_OBJECT)) { + Type superType = target.superClassType(); + ClassInfo superClass = index.getIndex().getClassByName(superType.name()); + extractMappings(classPropertyMapping, superClass, index); + } } } diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ProjectionForEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ProjectionForEnhancer.java new file mode 100644 index 0000000000000..b311da8ae704e --- /dev/null +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ProjectionForEnhancer.java @@ -0,0 +1,91 @@ +package io.quarkus.mongodb.panache.deployment; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class ProjectionForEnhancer implements BiFunction { + private static final String BSONPROPERTY_BINARY_NAME = "org/bson/codecs/pojo/annotations/BsonProperty"; + private static final String BSONPROPERTY_SIGNATURE = "L" + BSONPROPERTY_BINARY_NAME + ";"; + + private Map propertyMapping; + + public ProjectionForEnhancer(Map propertyMapping) { + this.propertyMapping = propertyMapping; + } + + @Override + public ClassVisitor apply(String className, ClassVisitor classVisitor) { + return new BsonPropertyClassVisitor(classVisitor, propertyMapping); + } + + static class BsonPropertyClassVisitor extends ClassVisitor { + Map propertyMapping; + + BsonPropertyClassVisitor(ClassVisitor outputClassVisitor, Map propertyMapping) { + super(Opcodes.ASM7, outputClassVisitor); + this.propertyMapping = propertyMapping; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldVisitor superVisitor = super.visitField(access, name, descriptor, signature, value); + if (this.propertyMapping.containsKey(name)) { + return new FieldVisitor(Opcodes.ASM7, superVisitor) { + private Set descriptors = new HashSet<>(); + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + descriptors.add(descriptor); + return super.visitAnnotation(descriptor, visible); + } + + @Override + public void visitEnd() { + if (!descriptors.contains(BSONPROPERTY_SIGNATURE)) { + AnnotationVisitor visitor = super.visitAnnotation(BSONPROPERTY_SIGNATURE, true); + visitor.visit("value", propertyMapping.get(name)); + visitor.visitEnd(); + } + super.visitEnd(); + } + }; + } + return superVisitor; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor superVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); + if (this.propertyMapping.containsKey(name)) { + return new MethodVisitor(Opcodes.ASM7, superVisitor) { + private Set descriptors = new HashSet<>(); + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + descriptors.add(descriptor); + return super.visitAnnotation(descriptor, visible); + } + + @Override + public void visitEnd() { + if (!descriptors.contains(BSONPROPERTY_SIGNATURE)) { + AnnotationVisitor visitor = super.visitAnnotation(BSONPROPERTY_SIGNATURE, true); + visitor.visit("value", propertyMapping.get(name)); + visitor.visitEnd(); + } + super.visitEnd(); + } + }; + } + return superVisitor; + } + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java index c4ed06d9573cf..f924fb22f09f4 100755 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import org.bson.Document; @@ -85,6 +86,17 @@ public static T findById(Object id) { throw MongoOperations.implementationInjectionMissing(); } + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @return if found, an optional containing the entity, else Optional.empty(). + */ + @GenerateBridge + public static Optional findByIdOptional(Object id) { + throw MongoOperations.implementationInjectionMissing(); + } + /** * Find entities using a query, with optional indexed parameters. * diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java index c99958fd3df43..532328334129c 100755 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import org.bson.Document; @@ -91,6 +92,17 @@ public default Entity findById(Id id) { throw MongoOperations.implementationInjectionMissing(); } + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @return if found, an optional containing the entity, else Optional.empty(). + */ + @GenerateBridge + public default Optional findByIdOptional(Id id) { + throw MongoOperations.implementationInjectionMissing(); + } + /** * Find entities using a query, with optional indexed parameters. * diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java index 3ba5f0dd381cd..9419bfaac0529 100755 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java @@ -1,6 +1,7 @@ package io.quarkus.mongodb.panache; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; import io.quarkus.panache.common.Page; @@ -18,6 +19,14 @@ public interface PanacheQuery { // Builder + /** + * Defines a projection class: the getters, and the public fields, will be used to restrict which fields should be + * retrieved from the database. + * + * @return this query, modified + */ + public PanacheQuery project(Class type); + /** * Sets the current page. * @@ -147,11 +156,30 @@ public interface PanacheQuery { */ public T firstResult(); + /** + * Returns the first result of the current page index. This ignores the current page size to fetch + * a single result. + * + * @return if found, an optional containing the entity, else Optional.empty(). + * @see #singleResultOptional() + */ + public Optional firstResultOptional(); + /** * Executes this query for the current page and return a single result. * - * @return the single result (throws if there is not exactly one) + * @return the single result + * @throws PanacheQueryException if there is not exactly one result. * @see #firstResult() */ public T singleResult(); + + /** + * Executes this query for the current page and return a single result. + * + * @return if found, an optional containing the entity, else Optional.empty(). + * @throws PanacheQueryException if there is more than one result. + * @see #firstResultOptional() + */ + public Optional singleResultOptional(); } diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/ProjectionFor.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/ProjectionFor.java new file mode 100644 index 0000000000000..6f74d7aab4a48 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/ProjectionFor.java @@ -0,0 +1,14 @@ +package io.quarkus.mongodb.panache; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ProjectionFor { + public Class value(); +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java index 8ab9230ad6d4a..f63874a48b44f 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java @@ -1,8 +1,10 @@ package io.quarkus.mongodb.panache.runtime; import java.text.SimpleDateFormat; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Date; @@ -34,6 +36,10 @@ static String escape(Object value) { LocalDateTime dateValue = (LocalDateTime) value; return "ISODate('" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dateValue) + "')"; } + if (value instanceof Instant) { + Instant dateValue = (Instant) value; + return "ISODate('" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dateValue.atZone(ZoneOffset.UTC)) + "')"; + } return "'" + value.toString().replace("\\", "\\\\").replace("'", "\\'") + "'"; } } diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java index 4584a9cacdc04..a79618984bdd9 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -282,6 +283,10 @@ public static Object findById(Class entityClass, Object id) { return collection.find(new Document(ID, id)).first(); } + public static Optional findByIdOptional(Class entityClass, Object id) { + return Optional.ofNullable(findById(entityClass, id)); + } + public static PanacheQuery find(Class entityClass, String query, Object... params) { return find(entityClass, query, null, params); } diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoPropertyUtil.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoPropertyUtil.java new file mode 100644 index 0000000000000..2b452f3f5f580 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoPropertyUtil.java @@ -0,0 +1,53 @@ +package io.quarkus.mongodb.panache.runtime; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.bson.codecs.pojo.annotations.BsonProperty; + +final class MongoPropertyUtil { + + private MongoPropertyUtil() { + //prevent initialization + } + + static Map extractReplacementMap(Class clazz) { + //TODO cache the replacement map or pre-compute it during build (using reflection or jandex) + Map replacementMap = new HashMap<>(); + for (Field field : clazz.getDeclaredFields()) { + BsonProperty bsonProperty = field.getAnnotation(BsonProperty.class); + if (bsonProperty != null) { + replacementMap.put(field.getName(), bsonProperty.value()); + } + } + for (Method method : clazz.getDeclaredMethods()) { + if (method.getName().startsWith("get")) { + // we try to replace also for getter + BsonProperty bsonProperty = method.getAnnotation(BsonProperty.class); + if (bsonProperty != null) { + String fieldName = decapitalize(method.getName().substring(3)); + replacementMap.put(fieldName, bsonProperty.value()); + } + } + } + return replacementMap; + } + + // copied from JavaBeanUtil that is inside the core deployment module so not accessible at runtime. + // See conventions expressed by https://docs.oracle.com/javase/7/docs/api/java/beans/Introspector.html#decapitalize(java.lang.String) + static String decapitalize(String name) { + if (name != null && name.length() != 0) { + if (name.length() > 1 && Character.isUpperCase(name.charAt(1))) { + return name; + } else { + char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + } else { + return name; + } + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java index 0110cc8ab93b6..06d36c0ba6ad1 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java @@ -1,14 +1,10 @@ package io.quarkus.mongodb.panache.runtime; -import java.beans.Introspector; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; -import org.bson.codecs.pojo.annotations.BsonProperty; import io.quarkus.panacheql.internal.HqlLexer; import io.quarkus.panacheql.internal.HqlParser; @@ -17,7 +13,7 @@ public class PanacheQlQueryBinder { public static String bindQuery(Class clazz, String query, Object[] params) { - Map replacementMap = extractReplacementMap(clazz); + Map replacementMap = MongoPropertyUtil.extractReplacementMap(clazz); //shorthand query if (params.length == 1 && query.indexOf('?') == -1) { @@ -35,10 +31,10 @@ public static String bindQuery(Class clazz, String query, Object[] params) { } public static String bindQuery(Class clazz, String query, Map params) { - Map replacementMap = extractReplacementMap(clazz); + Map replacementMap = MongoPropertyUtil.extractReplacementMap(clazz); Map parameterMaps = new HashMap<>(); - for (Map.Entry entry : params.entrySet()) { + for (Map.Entry entry : params.entrySet()) { String bindParamsKey = ":" + entry.getKey(); parameterMaps.put(bindParamsKey, entry.getValue()); } @@ -50,28 +46,6 @@ private static String replaceField(String field, Map replacement return replacementMap.getOrDefault(field, field); } - private static Map extractReplacementMap(Class clazz) { - //TODO cache the replacement map or pre-compute it during build (using reflection or jandex) - Map replacementMap = new HashMap<>(); - for (Field field : clazz.getDeclaredFields()) { - BsonProperty bsonProperty = field.getAnnotation(BsonProperty.class); - if (bsonProperty != null) { - replacementMap.put(field.getName(), bsonProperty.value()); - } - } - for (Method method : clazz.getDeclaredMethods()) { - if (method.getName().startsWith("get")) { - // we try to replace also for getter - BsonProperty bsonProperty = method.getAnnotation(BsonProperty.class); - if (bsonProperty != null) { - String fieldName = Introspector.decapitalize(method.getName().substring(3)); - replacementMap.put(fieldName, bsonProperty.value()); - } - } - } - return replacementMap; - } - private static String prepareQuery(String query, Map replacementMap, Map parameterMaps) { HqlLexer lexer = new HqlLexer(CharStreams.fromString(query)); CommonTokenStream tokens = new CommonTokenStream(lexer); diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java index 0b3938f2475bb..f41b2196d6b73 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java @@ -1,7 +1,13 @@ package io.quarkus.mongodb.panache.runtime; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; +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 org.bson.Document; @@ -12,12 +18,14 @@ import io.quarkus.mongodb.panache.PanacheQuery; import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.exception.PanacheQueryException; public class PanacheQueryImpl implements PanacheQuery { private MongoCollection collection; private Class entityClass; private Document mongoQuery; private Document sort; + private Document projections; /* * We store the pageSize and apply it for each request because getFirstResult() @@ -37,6 +45,40 @@ public class PanacheQueryImpl implements PanacheQuery { // Builder + @Override + public PanacheQuery project(Class type) { + Set fieldNames = new HashSet<>(); + // gather field names from getters + for (Method method : type.getMethods()) { + if (method.getName().startsWith("get")) { + String fieldName = MongoPropertyUtil.decapitalize(method.getName().substring(3)); + fieldNames.add(fieldName); + } + } + + // gather field names from public fields + for (Field field : type.getFields()) { + fieldNames.add(field.getName()); + } + + // replace fields that have @BsonProperty mappings + Map replacementMap = MongoPropertyUtil.extractReplacementMap(type); + for (Map.Entry entry : replacementMap.entrySet()) { + if (fieldNames.contains(entry.getKey())) { + fieldNames.remove(entry.getKey()); + fieldNames.add(entry.getValue()); + } + } + + // create the projection document + this.projections = new Document(); + for (String fieldName : fieldNames) { + this.projections.append(fieldName, 1); + } + + return (PanacheQuery) this; + } + @Override @SuppressWarnings("unchecked") public PanacheQuery page(Page page) { @@ -108,6 +150,9 @@ public long count() { public List list() { List list = new ArrayList<>(); FindIterable find = mongoQuery == null ? collection.find() : collection.find(mongoQuery); + if (this.projections != null) { + find.projection(projections); + } MongoCursor cursor = find.sort(sort).skip(page.index).limit(page.size).iterator(); try { @@ -133,14 +178,30 @@ public T firstResult() { return list.isEmpty() ? null : list.get(0); } + @Override + public Optional firstResultOptional() { + return Optional.ofNullable(firstResult()); + } + @Override @SuppressWarnings("unchecked") public T singleResult() { List list = list(); if (list.isEmpty() || list.size() > 1) { - throw new RuntimeException("There should be only one result");//TODO use proper exception + throw new PanacheQueryException("There should be only one result"); } return list.get(0); } + + @Override + @SuppressWarnings("unchecked") + public Optional singleResultOptional() { + List list = list(); + if (list.size() > 1) { + throw new PanacheQueryException("There should be no more than one result"); + } + + return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)); + } } diff --git a/extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java b/extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java index f8dc5d55b1df1..b7bab77eba36d 100644 --- a/extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java +++ b/extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.Date; import org.bson.codecs.pojo.annotations.BsonProperty; @@ -35,6 +36,10 @@ public void testBindShorthandQuery() { query = MongoOperations.bindQuery(Object.class, "field", new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1) }); assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "field", + new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1).toInstant(ZoneOffset.UTC) }); + assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "field", new Object[] { toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1)) }); assertEquals("{'field':ISODate('2019-03-04T01:01:01.000')}", query); @@ -68,6 +73,10 @@ public void testBindNativeQueryByIndex() { new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1) }); assertEquals("{'field': ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "{'field': ?1}", + new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1).toInstant(ZoneOffset.UTC) }); + assertEquals("{'field': ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "{'field': ?1}", new Object[] { toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1)) }); assertEquals("{'field': ISODate('2019-03-04T01:01:01.000')}", query); @@ -99,6 +108,10 @@ public void testBindNativeQueryByName() { Parameters.with("field", LocalDateTime.of(2019, 3, 4, 1, 1, 1)).map()); assertEquals("{'field': ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "{'field': :field}", + Parameters.with("field", LocalDateTime.of(2019, 3, 4, 1, 1, 1).toInstant(ZoneOffset.UTC)).map()); + assertEquals("{'field': ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "{'field': :field}", Parameters.with("field", toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1))).map()); assertEquals("{'field': ISODate('2019-03-04T01:01:01.000')}", query); @@ -127,6 +140,10 @@ public void testBindEnhancedQueryByIndex() { query = MongoOperations.bindQuery(Object.class, "field = ?1", new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1) }); assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "field = ?1", + new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1).toInstant(ZoneOffset.UTC) }); + assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "field = ?1", new Object[] { toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1)) }); assertEquals("{'field':ISODate('2019-03-04T01:01:01.000')}", query); @@ -180,6 +197,10 @@ public void testBindEnhancedQueryByName() { Parameters.with("field", LocalDateTime.of(2019, 3, 4, 1, 1, 1)).map()); assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "field = :field", + Parameters.with("field", LocalDateTime.of(2019, 3, 4, 1, 1, 1).toInstant(ZoneOffset.UTC)).map()); + assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + query = MongoOperations.bindQuery(Object.class, "field = :field", Parameters.with("field", toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1))).map()); assertEquals("{'field':ISODate('2019-03-04T01:01:01.000')}", query); diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/EntityField.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/EntityField.java index 8b53d73281ae9..703c4bdc1d1ca 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/EntityField.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/EntityField.java @@ -1,5 +1,16 @@ package io.quarkus.panache.common.deployment; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.MethodVisitor; + import io.quarkus.deployment.bean.JavaBeanUtil; public class EntityField { @@ -7,6 +18,7 @@ public class EntityField { public final String name; public final String descriptor; public String signature; + public final Set annotations = new HashSet<>(2); public EntityField(String name, String descriptor) { this.name = name; @@ -21,4 +33,34 @@ public String getSetterName() { return JavaBeanUtil.getSetterName(name); } + public static class EntityFieldAnnotation { + public final String descriptor; + public final Map attributes = new HashMap<>(2); + public final List nestedAnnotations = new ArrayList<>(2); + + public EntityFieldAnnotation(String desc) { + this.descriptor = desc; + } + + public void writeToVisitor(MethodVisitor mv) { + AnnotationVisitor av = mv.visitAnnotation(descriptor, true); + for (Entry e : attributes.entrySet()) { + av.visit(e.getKey(), e.getValue()); + } + if (!nestedAnnotations.isEmpty()) { + // Always visit nested annotations as an array because this covers JAX-B annotations + AnnotationVisitor nestedVisitor = av.visitArray("value"); + for (EntityFieldAnnotation nestedAnno : nestedAnnotations) { + AnnotationVisitor arrayAnnoVisitor = nestedVisitor.visitAnnotation(null, nestedAnno.descriptor); + for (Entry e : nestedAnno.attributes.entrySet()) { + arrayAnnoVisitor.visit(e.getKey(), e.getValue()); + } + arrayAnnoVisitor.visitEnd(); + } + nestedVisitor.visitEnd(); + } + av.visitEnd(); + } + } + } diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java index 9e756db4bec9a..c26c31e49c7ab 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java @@ -21,6 +21,7 @@ import io.quarkus.panache.common.Parameters; import io.quarkus.panache.common.Sort; +import io.quarkus.panache.common.deployment.EntityField.EntityFieldAnnotation; public abstract class PanacheEntityEnhancer> implements BiFunction { @@ -33,6 +34,7 @@ public abstract class PanacheEntityEnhancer typeParameters = io.quarkus.deployment.util.JandexUtil + .resolveTypeParameters(clazz, repositoryDotName, indexView); + if (typeParameters.isEmpty()) + throw new IllegalStateException( + "Failed to find supertype " + repositoryDotName + " from entity class " + clazz); + org.jboss.jandex.Type entityType = typeParameters.get(0); + return entityType.name().toString().replace('.', '/'); } @Override @@ -167,4 +166,11 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE mv.visitEnd(); } } + + public static boolean skipRepository(ClassInfo classInfo) { + // we don't want to add methods to abstract/generic entities/repositories: they get added to bottom types + // which can't be either + return Modifier.isAbstract(classInfo.flags()) + || !classInfo.typeParameters().isEmpty(); + } } diff --git a/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/exception/PanacheQueryException.java b/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/exception/PanacheQueryException.java new file mode 100644 index 0000000000000..5cfc1a925702e --- /dev/null +++ b/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/exception/PanacheQueryException.java @@ -0,0 +1,7 @@ +package io.quarkus.panache.common.exception; + +public class PanacheQueryException extends RuntimeException { + public PanacheQueryException(String s) { + super(s); + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index be34e299fd3f5..31e02abe26ca8 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -19,6 +19,9 @@ scheduler quartz + + config-yaml + jackson jaxb @@ -45,6 +48,7 @@ resteasy-jsonb resteasy-jackson resteasy-jaxb + resteasy-qute rest-client smallrye-openapi-common smallrye-openapi @@ -74,6 +78,7 @@ panache hibernate-search-elasticsearch elasticsearch-rest-client + jsch jgit kafka-client kafka-streams @@ -88,11 +93,14 @@ spring-web spring-data-jpa spring-security + spring-boot-properties security + elytron-security-common elytron-security elytron-security-jdbc + elytron-security-ldap elytron-security-properties-file elytron-security-oauth2 smallrye-jwt @@ -129,6 +137,14 @@ vault vertx-graphql + + + logging-json + logging-sentry + logging-gelf + + + qute - + quarkus-quartz + Quarkus - Quartz - Runtime + Schedule clustered tasks with Quartz + + + io.quarkus + quarkus-agroal + true + + + io.quarkus + quarkus-scheduler + + + com.oracle.substratevm + svm + + + org.quartz-scheduler + quartz + + + com.zaxxer + HikariCP-java6 + + + com.mchange + c3p0 + + + + + jakarta.transaction + jakarta.transaction-api + + - + - - - - io.quarkus - quarkus-bootstrap-maven-plugin - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuarkusQuartzConnectionPoolProvider.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuarkusQuartzConnectionPoolProvider.java new file mode 100644 index 0000000000000..e39fd122baec5 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuarkusQuartzConnectionPoolProvider.java @@ -0,0 +1,78 @@ +package io.quarkus.quartz.runtime; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.enterprise.util.AnnotationLiteral; +import javax.sql.DataSource; + +import org.quartz.utils.PoolingConnectionProvider; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; + +public class QuarkusQuartzConnectionPoolProvider implements PoolingConnectionProvider { + private AgroalDataSource dataSource; + private static String dataSourceName; + + public QuarkusQuartzConnectionPoolProvider() { + final ArcContainer container = Arc.container(); + final InstanceHandle instanceHandle; + final boolean useDefaultDataSource = "QUARKUS_QUARTZ_DEFAULT_DATASOURCE".equals(dataSourceName); + if (useDefaultDataSource) { + instanceHandle = container.instance(AgroalDataSource.class); + } else { + instanceHandle = container.instance(AgroalDataSource.class, new DataSourceLiteral(dataSourceName)); + } + if (instanceHandle.isAvailable()) { + this.dataSource = instanceHandle.get(); + } else { + String message = String.format( + "JDBC Store configured but '%s' datasource is missing. You can configure your datasource by following the guide available at: https://quarkus.io/guides/datasource", + useDefaultDataSource ? "default" : dataSourceName); + throw new IllegalStateException(message); + } + } + + @Override + public DataSource getDataSource() { + return dataSource; + } + + @Override + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + @Override + public void shutdown() { + // Do nothing as the connection will be closed inside the Agroal extension + } + + @Override + public void initialize() { + + } + + static void setDataSourceName(String dataSourceName) { + QuarkusQuartzConnectionPoolProvider.dataSourceName = dataSourceName; + } + + private static class DataSourceLiteral extends AnnotationLiteral + implements io.quarkus.agroal.DataSource { + + private String name; + + public DataSourceLiteral(String name) { + this.name = name; + } + + @Override + public String value() { + return name; + } + + } +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java new file mode 100644 index 0000000000000..4411910f4a0f0 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java @@ -0,0 +1,41 @@ +package io.quarkus.quartz.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class QuartzBuildTimeConfig { + /** + * Enable cluster mode or not. + *

+ * If enabled make sure to set the appropriate cluster properties. + */ + @ConfigItem + public boolean clustered; + + /** + * The type of store to use. + *

+ * When using the `db` store type configuration value make sure that you have the datasource configured. + * See Configuring your datasource for more information. + *

+ * To create Quartz tables, you can perform a schema migration via the Flyway + * extension using a SQL script matching your database picked from Quartz + * repository. + */ + @ConfigItem(defaultValue = "ram") + public StoreType storeType; + + /** + * The name of the datasource to use. + *

+ * Optionally needed when using the `db` store type. + * If not specified, defaults to using the default datasource. + */ + @ConfigItem(name = "datasource") + public Optional dataSourceName; +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java index d694e8fe5ac1a..cbbd0b4892d2e 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java @@ -1,14 +1,17 @@ package io.quarkus.quartz.runtime; +import java.util.Optional; + import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.annotations.Recorder; @Recorder public class QuartzRecorder { - public void initialize(QuartzRuntimeConfig runtimeConfig, BeanContainer container) { + public void initialize(QuartzRuntimeConfig runTimeConfig, QuartzBuildTimeConfig buildTimeConfig, BeanContainer container, + Optional driverDialect) { QuartzSupport support = container.instance(QuartzSupport.class); - support.initialize(runtimeConfig); + support.initialize(runTimeConfig, buildTimeConfig, driverDialect); } } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java index 1198bc60441a3..0ed976fce018f 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java @@ -9,6 +9,8 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.BeforeDestroyed; import javax.enterprise.event.Observes; import javax.inject.Singleton; @@ -17,8 +19,8 @@ import org.quartz.CronScheduleBuilder; import org.quartz.Job; import org.quartz.JobBuilder; +import org.quartz.JobDetail; import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; import org.quartz.ScheduleBuilder; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; @@ -55,7 +57,6 @@ public class QuartzScheduler implements Scheduler { private final Map invokers; public QuartzScheduler(SchedulerSupport schedulerSupport, QuartzSupport quartzSupport, Config config) { - if (schedulerSupport.getScheduledMethods().isEmpty()) { this.triggerNameSequence = null; this.scheduler = null; @@ -66,21 +67,7 @@ public QuartzScheduler(SchedulerSupport schedulerSupport, QuartzSupport quartzSu this.invokers = new HashMap<>(); try { - Properties props = new Properties(); - props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, "QuarkusQuartzScheduler"); - props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "QuarkusQuartzScheduler"); - props.put(StdSchedulerFactory.PROP_SCHED_WRAP_JOB_IN_USER_TX, false); - props.put(StdSchedulerFactory.PROP_SCHED_SCHEDULER_THREADS_INHERIT_CONTEXT_CLASS_LOADER_OF_INITIALIZING_THREAD, - true); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool"); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", - "" + quartzSupport.getRuntimeConfig().threadCount); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadPriority", - "" + quartzSupport.getRuntimeConfig().threadPriority); - props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", "60000"); - props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, "org.quartz.simpl.RAMJobStore"); - props.put(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT, false); - props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, false); + Properties props = getSchedulerConfigurationProperties(quartzSupport); SchedulerFactory schedulerFactory = new StdSchedulerFactory(props); scheduler = schedulerFactory.getScheduler(); @@ -109,8 +96,9 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler scheduler) thr for (Scheduled scheduled : method.getSchedules()) { String name = triggerNameSequence.getAndIncrement() + "_" + method.getInvokerClassName(); JobBuilder jobBuilder = JobBuilder.newJob(InvokerJob.class) - .withIdentity(name, Scheduler.class.getName()).usingJobData(INVOKER_KEY, - method.getInvokerClassName()); + .withIdentity(name, Scheduler.class.getName()) + .usingJobData(INVOKER_KEY, method.getInvokerClassName()) + .requestRecovery(); ScheduleBuilder scheduleBuilder; String cron = scheduled.cron().trim(); @@ -161,8 +149,13 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler scheduler) thr .plusMillis(scheduled.delayUnit().toMillis(scheduled.delay())).toEpochMilli())); } - scheduler.scheduleJob(jobBuilder.build(), triggerBuilder.build()); - LOGGER.debugf("Scheduled business method %s with config %s", method.getMethodDescription(), scheduled); + JobDetail job = jobBuilder.build(); + if (scheduler.checkExists(job.getKey())) { + scheduler.deleteJob(job.getKey()); + } + scheduler.scheduleJob(job, triggerBuilder.build()); + LOGGER.debugf("Scheduled business method %s with config %s", method.getMethodDescription(), + scheduled); } } } catch (SchedulerException e) { @@ -204,21 +197,76 @@ void start(@Observes StartupEvent startupEvent) { } } + /** + * Need to gracefully shutdown the scheduler making sure that all triggers have been + * released before datasource shutdown. + * + * @param event ignored + */ + void destroy(@BeforeDestroyed(ApplicationScoped.class) Object event) { // + if (scheduler != null) { + try { + scheduler.shutdown(true); // gracefully shutdown + } catch (SchedulerException e) { + LOGGER.warnf("Unable to gracefully shutdown the scheduler", e); + } + } + } + @PreDestroy void destroy() { if (scheduler != null) { try { - scheduler.shutdown(); + if (!scheduler.isShutdown()) { + scheduler.shutdown(false); // force shutdown + } } catch (SchedulerException e) { - LOGGER.warnf("Unable to shutdown scheduler", e); + LOGGER.warnf("Unable to shutdown the scheduler", e); } } } + private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSupport) { + Properties props = new Properties(); + QuartzBuildTimeConfig buildTimeConfig = quartzSupport.getBuildTimeConfig(); + props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, "AUTO"); + props.put("org.quartz.scheduler.skipUpdateCheck", "true"); + props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "QuarkusQuartzScheduler"); + props.put(StdSchedulerFactory.PROP_SCHED_WRAP_JOB_IN_USER_TX, "false"); + props.put(StdSchedulerFactory.PROP_SCHED_SCHEDULER_THREADS_INHERIT_CONTEXT_CLASS_LOADER_OF_INITIALIZING_THREAD, "true"); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool"); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", + "" + quartzSupport.getRuntimeConfig().threadCount); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadPriority", + "" + quartzSupport.getRuntimeConfig().threadPriority); + props.put(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT, "false"); + props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, "false"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, buildTimeConfig.storeType.clazz); + + if (buildTimeConfig.storeType == StoreType.DB) { + String dataSource = buildTimeConfig.dataSourceName.orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE"); + QuarkusQuartzConnectionPoolProvider.setDataSourceName(dataSource); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".useProperties", "true"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", "60000"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".tablePrefix", "QRTZ_"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".dataSource", dataSource); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".driverDelegateClass", + quartzSupport.getDriverDialect().get()); + props.put(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dataSource + ".connectionProvider.class", + QuarkusQuartzConnectionPoolProvider.class.getName()); + if (buildTimeConfig.clustered) { + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".isClustered", "true"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".clusterCheckinInterval", "20000"); // 20 seconds + } + } + + return props; + } + class InvokerJob implements Job { @Override - public void execute(JobExecutionContext context) throws JobExecutionException { + public void execute(JobExecutionContext context) { Trigger trigger = new Trigger() { @Override @@ -239,23 +287,25 @@ public String getId() { } }; String invokerClass = context.getJobDetail().getJobDataMap().getString(INVOKER_KEY); - invokers.get(invokerClass).invoke(new ScheduledExecution() { - - @Override - public Trigger getTrigger() { - return trigger; - } + ScheduledInvoker scheduledInvoker = invokers.get(invokerClass); + if (scheduledInvoker != null) { // could be null from previous runs + scheduledInvoker.invoke(new ScheduledExecution() { + @Override + public Trigger getTrigger() { + return trigger; + } - @Override - public Instant getScheduledFireTime() { - return context.getScheduledFireTime().toInstant(); - } + @Override + public Instant getScheduledFireTime() { + return context.getScheduledFireTime().toInstant(); + } - @Override - public Instant getFireTime() { - return context.getFireTime().toInstant(); - } - }); + @Override + public Instant getFireTime() { + return context.getFireTime().toInstant(); + } + }); + } } } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java index 53a908533ef6e..c510e585d4973 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java @@ -1,18 +1,31 @@ package io.quarkus.quartz.runtime; +import java.util.Optional; + import javax.inject.Singleton; @Singleton public class QuartzSupport { private QuartzRuntimeConfig runtimeConfig; + private QuartzBuildTimeConfig buildTimeConfig; + private Optional driverDialect; - void initialize(QuartzRuntimeConfig runtimeConfig) { - this.runtimeConfig = runtimeConfig; + void initialize(QuartzRuntimeConfig runTimeConfig, QuartzBuildTimeConfig buildTimeConfig, Optional driverDialect) { + this.runtimeConfig = runTimeConfig; + this.buildTimeConfig = buildTimeConfig; + this.driverDialect = driverDialect; } public QuartzRuntimeConfig getRuntimeConfig() { return runtimeConfig; } + public QuartzBuildTimeConfig getBuildTimeConfig() { + return buildTimeConfig; + } + + public Optional getDriverDialect() { + return driverDialect; + } } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java new file mode 100644 index 0000000000000..fb43eebbc3289 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java @@ -0,0 +1,17 @@ +package io.quarkus.quartz.runtime; + +import org.quartz.impl.jdbcjobstore.JobStoreTX; +import org.quartz.simpl.RAMJobStore; + +public enum StoreType { + RAM(RAMJobStore.class.getName(), RAMJobStore.class.getSimpleName()), + DB(JobStoreTX.class.getName(), JobStoreTX.class.getSimpleName()); + + public String name; + public String clazz; + + StoreType(String clazz, String name) { + this.clazz = clazz; + this.name = name; + } +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java index c408fb2f1fcd2..07cc9ced71ee5 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java @@ -1,8 +1,11 @@ package io.quarkus.quartz.runtime.graal; +import java.io.ByteArrayOutputStream; import java.rmi.RemoteException; +import java.sql.ResultSet; import org.quartz.core.RemotableQuartzScheduler; +import org.quartz.impl.jdbcjobstore.StdJDBCDelegate; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; @@ -37,5 +40,30 @@ protected RemotableQuartzScheduler getRemoteScheduler() { } +@TargetClass(StdJDBCDelegate.class) +final class Target_org_quartz_impl_jdbc_jobstore_StdJDBCDelegate { + + /** + * Activate the usage of {@link java.util.Properties} to avoid Object serialization + * which is not supported by GraalVM - see https://github.com/oracle/graal/issues/460 + * + * @return true + */ + @Substitute + protected boolean canUseProperties() { + return true; + } + + @Substitute + protected ByteArrayOutputStream serializeObject(Object obj) { + throw new IllegalStateException("Object serialization not supported."); // should not reach here + } + + @Substitute + protected Object getObjectFromBlob(ResultSet rs, String colName) { + throw new IllegalStateException("Object serialization not supported."); // should not reach here + } +} + final class QuartzSubstitutions { } diff --git a/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml index a518bc3dfed0f..b34bb338a30c2 100644 --- a/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -1,11 +1,12 @@ --- -name: "Scheduler - tasks" +name: "Quartz" metadata: keywords: - "scheduler" + - "quartz" - "tasks" - "periodic-tasks" - guide: "https://quarkus.io/guides/scheduler" + guide: "https://quarkus.io/guides/quartz" categories: - "miscellaneous" status: "preview" diff --git a/extensions/qute/deployment/pom.xml b/extensions/qute/deployment/pom.xml new file mode 100644 index 0000000000000..c76413e76191a --- /dev/null +++ b/extensions/qute/deployment/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + quarkus-qute-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-qute-deployment + Quarkus - Qute - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus.qute + qute-generator + + + io.quarkus + quarkus-qute + + + io.quarkus + quarkus-junit5-internal + + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedValueResolverBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedValueResolverBuildItem.java new file mode 100644 index 0000000000000..aaa526dfc7478 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedValueResolverBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.qute.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Holds a name of a generated {@link io.quarkus.qute.ValueResolver} class. + */ +public final class GeneratedValueResolverBuildItem extends MultiBuildItem { + + private final String className; + + public GeneratedValueResolverBuildItem(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/ImplicitValueResolverBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/ImplicitValueResolverBuildItem.java new file mode 100644 index 0000000000000..0708dd4c92264 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/ImplicitValueResolverBuildItem.java @@ -0,0 +1,19 @@ +package io.quarkus.qute.deployment; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class ImplicitValueResolverBuildItem extends MultiBuildItem { + + private final ClassInfo clazz; + + public ImplicitValueResolverBuildItem(ClassInfo clazz) { + this.clazz = clazz; + } + + public ClassInfo getClazz() { + return clazz; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/IncorrectExpressionBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/IncorrectExpressionBuildItem.java new file mode 100644 index 0000000000000..77a86250f32ec --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/IncorrectExpressionBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.qute.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class IncorrectExpressionBuildItem extends MultiBuildItem { + + public final String expression; + public final String property; + public final String clazz; + public final int line; + public final String templateId; + + public IncorrectExpressionBuildItem(String expression, String property, String clazz, int line, String templateId) { + this.expression = expression; + this.property = property; + this.clazz = clazz; + this.line = line; + this.templateId = templateId; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java new file mode 100644 index 0000000000000..3ee57fc28ec97 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -0,0 +1,910 @@ +package io.quarkus.qute.deployment; + +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; +import static java.util.stream.Collectors.toMap; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; +import org.jboss.logging.Logger; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext; +import io.quarkus.arc.processor.BeanInfo; +import io.quarkus.arc.processor.BuildExtension; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.arc.processor.InjectionPointInfo; +import io.quarkus.deployment.ApplicationArchive; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.qute.Engine; +import io.quarkus.qute.Expression; +import io.quarkus.qute.LoopSectionHelper; +import io.quarkus.qute.PublisherFactory; +import io.quarkus.qute.ResultNode; +import io.quarkus.qute.SectionHelper; +import io.quarkus.qute.SectionHelperFactory; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.TemplateLocator; +import io.quarkus.qute.Variant; +import io.quarkus.qute.api.ResourcePath; +import io.quarkus.qute.api.VariantTemplate; +import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; +import io.quarkus.qute.generator.ExtensionMethodGenerator; +import io.quarkus.qute.generator.ValueResolverGenerator; +import io.quarkus.qute.runtime.DefaultTemplateExtensions; +import io.quarkus.qute.runtime.EngineProducer; +import io.quarkus.qute.runtime.QuteConfig; +import io.quarkus.qute.runtime.QuteRecorder; +import io.quarkus.qute.runtime.TemplateProducer; +import io.quarkus.qute.runtime.VariantTemplateProducer; +import io.quarkus.qute.rxjava.RxjavaPublisherFactory; + +public class QuteProcessor { + + private static final Logger LOGGER = Logger.getLogger(QuteProcessor.class); + + public static final DotName RESOURCE_PATH = DotName.createSimple(ResourcePath.class.getName()); + public static final DotName TEMPLATE = DotName.createSimple(Template.class.getName()); + public static final DotName VARIANT_TEMPLATE = DotName.createSimple(VariantTemplate.class.getName()); + + static final DotName ITERABLE = DotName.createSimple(Iterable.class.getName()); + static final DotName STREAM = DotName.createSimple(Stream.class.getName()); + static final DotName MAP = DotName.createSimple(Map.class.getName()); + static final DotName MAP_ENTRY = DotName.createSimple(Entry.class.getName()); + + private static final String MATCH_NAME = "matchName"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FeatureBuildItem.QUTE); + } + + @BuildStep + void processTemplateErrors(TemplatesAnalysisBuildItem analysis, List incorrectExpressions, + BuildProducer serviceStart) { + + List errors = new ArrayList<>(); + + for (IncorrectExpressionBuildItem incorrectExpression : incorrectExpressions) { + if (incorrectExpression.clazz != null) { + errors.add(String.format( + "Incorrect expression: %s\n\t- property [%s] not found on class [%s] nor handled by an extension method\n\t- found in template [%s] on line %s", + incorrectExpression.expression, incorrectExpression.property, incorrectExpression.clazz, + findTemplatePath(analysis, incorrectExpression.templateId), incorrectExpression.line)); + } else { + errors.add(String.format( + "Incorrect expression %s\n\t @Named bean not found for [%s]\n\t- found in template [%s] on line %s", + incorrectExpression.expression, incorrectExpression.property, + findTemplatePath(analysis, incorrectExpression.templateId), incorrectExpression.line)); + } + } + + if (!errors.isEmpty()) { + StringBuilder message = new StringBuilder("Found " + errors.size() + " template problems: "); + int idx = 1; + for (String errorMessage : errors) { + message.append("\n").append("[").append(idx++).append("] ").append(errorMessage); + } + throw new TemplateException(message.toString()); + } + } + + @BuildStep + AdditionalBeanBuildItem additionalBeans() { + return AdditionalBeanBuildItem.builder() + .setUnremovable() + .addBeanClasses(EngineProducer.class, TemplateProducer.class, VariantTemplateProducer.class, ResourcePath.class, + Template.class, TemplateInstance.class, DefaultTemplateExtensions.class) + .build(); + } + + @BuildStep + TemplatesAnalysisBuildItem analyzeTemplates(List templatePaths) { + long start = System.currentTimeMillis(); + List analysis = new ArrayList<>(); + + // A dummy engine instance is used to parse and validate all templates during the build. The real engine instance is created at startup. + Engine dummyEngine = Engine.builder().addDefaultSectionHelpers().computeSectionHelper(name -> { + // Create a dummy section helper factory for an uknown section that could be potentially registered at runtime + return new SectionHelperFactory() { + @Override + public SectionHelper initialize(SectionInitContext context) { + return new SectionHelper() { + @Override + public CompletionStage resolve(SectionResolutionContext context) { + return CompletableFuture.completedFuture(ResultNode.NOOP); + } + }; + } + }; + }).addLocator(new TemplateLocator() { + @Override + public Optional locate(String id) { + TemplatePathBuildItem found = templatePaths.stream().filter(p -> p.getPath().equals(id)).findAny().orElse(null); + if (found != null) { + try { + byte[] content = Files.readAllBytes(found.getFullPath()); + return Optional.of(new TemplateLocation() { + @Override + public Reader read() { + return new StringReader(new String(content, StandardCharsets.UTF_8)); + } + + @Override + public Optional getVariant() { + return Optional.empty(); + } + }); + } catch (IOException e) { + LOGGER.warn("Unable to read the template from path: " + found.getFullPath(), e); + } + } + ; + return Optional.empty(); + } + }).build(); + + for (TemplatePathBuildItem path : templatePaths) { + Template template = dummyEngine.getTemplate(path.getPath()); + if (template != null) { + analysis.add(new TemplateAnalysis(template.getGeneratedId(), template.getExpressions(), path)); + } + } + LOGGER.debugf("Finished analysis of %s templates in %s ms", + analysis.size(), System.currentTimeMillis() - start); + return new TemplatesAnalysisBuildItem(analysis); + } + + @BuildStep + void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BeanArchiveIndexBuildItem beanArchiveIndex, + List templateExtensionMethods, + List excludes, + BuildProducer incorrectExpressions, + BuildProducer requiredClasses) { + + IndexView index = beanArchiveIndex.getIndex(); + Function templateIdToPathFun = new Function() { + @Override + public String apply(String id) { + return findTemplatePath(templatesAnalysis, id); + } + }; + + for (TemplateAnalysis analysis : templatesAnalysis.getAnalysis()) { + for (Expression expression : analysis.expressions) { + if (expression.typeCheckInfo == null) { + continue; + } + TypeCheckInfo typeCheckInfo = TypeCheckInfo.create(expression, index, templateIdToPathFun); + if (typeCheckInfo.parts.isEmpty()) { + continue; + } + Iterator parts = typeCheckInfo.parts.iterator(); + Match match = new Match(); + match.clazz = typeCheckInfo.rawClass; + match.type = typeCheckInfo.resolvedType; + + String rootHint = typeCheckInfo.getHelperHint(TypeCheckInfo.ROOT_HINT); + if (rootHint != null) { + processHints(rootHint, match, index); + } + + while (parts.hasNext()) { + // Now iterate over all parts of the expression and check each part against the current "match class" + String name = parts.next(); + if (match.clazz != null) { + requiredClasses.produce(new ImplicitValueResolverBuildItem(match.clazz)); + // TODO we don't validate virtual methods atm + if (name.contains("(")) { + break; + } + AnnotationTarget member = findProperty(name, match.clazz, index); + if (member == null) { + member = findTemplateExtensionMethod(name, match.clazz, templateExtensionMethods); + } + if (member == null && excludes.stream().anyMatch(e -> e.getPredicate().test(name, match.clazz))) { + LOGGER.debugf("No property found for %s in [%s] but it is intentionally ignored", name, + expression.toOriginalString(), match.clazz); + break; + } + if (member == null) { + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + name, match.clazz.toString(), expression.origin.getLine(), + expression.origin.getTemplateId())); + break; + } else { + match.type = resolveType(member, match, index); + if (match.type.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) { + break; + } + match.clazz = index.getClassByName(match.type.name()); + + String helperHint = typeCheckInfo.getHelperHint(name); + if (helperHint != null) { + // For example a loop section needs to validate the type of an element + processHints(helperHint, match, index); + } + } + } else { + // No match class - skip further validation + break; + } + } + } + } + } + + @BuildStep + void collectTemplateExtensionMethods(BeanArchiveIndexBuildItem beanArchiveIndex, + BuildProducer extensionMethods) { + + IndexView index = beanArchiveIndex.getIndex(); + Map methods = new HashMap<>(); + Map classes = new HashMap<>(); + + for (AnnotationInstance templateExtension : index.getAnnotations(ExtensionMethodGenerator.TEMPLATE_EXTENSION)) { + if (templateExtension.target().kind() == Kind.METHOD) { + methods.put(templateExtension.target().asMethod(), templateExtension); + } else if (templateExtension.target().kind() == Kind.CLASS) { + classes.put(templateExtension.target().asClass(), templateExtension); + } + } + + for (Entry entry : methods.entrySet()) { + MethodInfo method = entry.getKey(); + ExtensionMethodGenerator.validate(method); + produceExtensionMethod(index, extensionMethods, method, entry.getValue()); + LOGGER.debugf("Found template extension method %s declared on %s", method, + method.declaringClass().name()); + } + + for (Entry entry : classes.entrySet()) { + ClassInfo clazz = entry.getKey(); + for (MethodInfo method : clazz.methods()) { + if (!Modifier.isStatic(method.flags()) || method.returnType().kind() == org.jboss.jandex.Type.Kind.VOID + || method.parameters().isEmpty() || Modifier.isPrivate(method.flags()) || methods.containsKey(method)) { + continue; + } + produceExtensionMethod(index, extensionMethods, method, entry.getValue()); + LOGGER.debugf("Found template extension method %s declared on %s", method, + method.declaringClass().name()); + } + } + } + + private void produceExtensionMethod(IndexView index, BuildProducer extensionMethods, + MethodInfo method, AnnotationInstance extensionAnnotation) { + String matchName = null; + AnnotationValue matchNameValue = extensionAnnotation.value(MATCH_NAME); + if (matchNameValue != null) { + matchName = matchNameValue.asString(); + } + if (matchName == null) { + matchName = method.name(); + } + extensionMethods.produce(new TemplateExtensionMethodBuildItem(method, matchName, + index.getClassByName(method.parameters().get(0).name()))); + } + + @BuildStep + void validateBeansInjectedInTemplates(ApplicationArchivesBuildItem applicationArchivesBuildItem, + TemplatesAnalysisBuildItem analysis, BeanArchiveIndexBuildItem beanArchiveIndex, + BuildProducer incorrectExpressions, + List templateExtensionMethods, + List excludes, + ValidationPhaseBuildItem validationPhase, + BuildProducer validationError, + BuildProducer requiredClasses) { + + IndexView index = beanArchiveIndex.getIndex(); + Function templateIdToPathFun = new Function() { + @Override + public String apply(String id) { + return findTemplatePath(analysis, id); + } + }; + Set injectExpressions = collectInjectExpressions(analysis); + + if (!injectExpressions.isEmpty()) { + ValidationContext context = validationPhase.getContext(); + + Map namedBeans = context.get(BuildExtension.Key.BEANS).stream() + .filter(b -> b.getName() != null).collect(toMap(BeanInfo::getName, Function.identity())); + + Set expressions = collectInjectExpressions(analysis); + for (Expression expression : expressions) { + + String beanName = expression.parts.get(0); + BeanInfo bean = namedBeans.get(beanName); + if (bean != null) { + if (expression.parts.size() == 1) { + continue; + } + TypeCheckInfo typeCheckInfo = TypeCheckInfo.create(expression, index, templateIdToPathFun); + if (typeCheckInfo.parts.isEmpty()) { + continue; + } + + Iterator parts = expression.parts.listIterator(1); + Match match = new Match(); + match.clazz = bean.getImplClazz(); + + while (parts.hasNext()) { + // Now iterate over all parts of the expression and check each part against the current "match class" + String name = parts.next(); + if (match.clazz != null) { + requiredClasses.produce(new ImplicitValueResolverBuildItem(match.clazz)); + + // TODO we don't validate virtual methods atm + if (name.contains("(")) { + break; + } + AnnotationTarget member = findProperty(name, match.clazz, index); + if (member == null) { + member = findTemplateExtensionMethod(name, match.clazz, templateExtensionMethods); + } + if (member == null + && excludes.stream().anyMatch(e -> e.getPredicate().test(name, match.clazz))) { + LOGGER.debugf("No property found for %s in [%s] but it is intentionally ignored", name, + expression.toOriginalString(), match.clazz); + break; + } + if (member == null) { + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + name, match.clazz.toString(), expression.origin.getLine(), + expression.origin.getTemplateId())); + break; + } else { + if (member.kind() == Kind.FIELD) { + match.type = member.asField().type(); + } else if (member.kind() == Kind.METHOD) { + match.type = member.asMethod().returnType(); + } else { + throw new IllegalStateException("Unsupported member type: " + member); + } + if (match.type.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) { + break; + } + match.clazz = index.getClassByName(match.type.name()); + + String helperHint = typeCheckInfo.getHelperHint(name); + if (helperHint != null) { + // For example loop section needs to validate the type of an element + processHints(helperHint, match, index); + } + } + } else { + // No match class - skip further validation + break; + } + } + + } else { + // User is injecting a non-existing bean + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + beanName, null, expression.origin.getLine(), + expression.origin.getTemplateId())); + } + } + } + } + + private String findTemplatePath(TemplatesAnalysisBuildItem analysis, String id) { + for (TemplateAnalysis templateAnalysis : analysis.getAnalysis()) { + if (templateAnalysis.id.equals(id)) { + return templateAnalysis.path.getPath(); + } + } + return null; + } + + @BuildStep + void generateValueResolvers(QuteConfig config, BuildProducer generatedClass, + CombinedIndexBuildItem combinedIndex, BeanArchiveIndexBuildItem beanArchiveIndex, + ApplicationArchivesBuildItem applicationArchivesBuildItem, + List templatePaths, + List templateExtensionMethods, + List requiredClasses, + TemplatesAnalysisBuildItem templatesAnalysis, + BuildProducer generatedResolvers, + BuildProducer reflectiveClass) { + + IndexView index = beanArchiveIndex.getIndex(); + Predicate appClassPredicate = new Predicate() { + @Override + public boolean test(String name) { + if (applicationArchivesBuildItem.getRootArchive().getIndex() + .getClassByName(DotName.createSimple(name)) != null) { + return true; + } + // TODO generated classes? + return false; + } + }; + ClassOutput classOutput = new ClassOutput() { + @Override + public void write(String name, byte[] data) { + int idx = name.lastIndexOf(ExtensionMethodGenerator.SUFFIX); + if (idx == -1) { + idx = name.lastIndexOf(ValueResolverGenerator.SUFFIX); + } + String className = name.substring(0, idx).replace("/", "."); + if (className.contains(ValueResolverGenerator.NESTED_SEPARATOR)) { + className = className.replace(ValueResolverGenerator.NESTED_SEPARATOR, "$"); + } + boolean appClass = appClassPredicate.test(className); + LOGGER.debugf("Writing %s [appClass=%s]", name, appClass); + generatedClass.produce(new GeneratedClassBuildItem(appClass, name, data)); + } + }; + + Set controlled = new HashSet<>(); + Map uncontrolled = new HashMap<>(); + for (AnnotationInstance templateData : index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA)) { + processsTemplateData(index, templateData, templateData.target(), controlled, uncontrolled); + } + for (AnnotationInstance containerInstance : index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA_CONTAINER)) { + for (AnnotationInstance templateData : containerInstance.value().asNestedArray()) { + processsTemplateData(index, templateData, containerInstance.target(), controlled, uncontrolled); + } + } + for (ImplicitValueResolverBuildItem required : requiredClasses) { + if (!controlled.contains(required.getClazz()) && !uncontrolled.containsKey(required.getClazz())) { + controlled.add(required.getClazz()); + } + } + + ValueResolverGenerator generator = ValueResolverGenerator.builder().setIndex(index).setClassOutput(classOutput) + .setUncontrolled(uncontrolled) + .build(); + + // @TemplateData + for (ClassInfo data : controlled) { + generator.generate(data); + } + // Uncontrolled classes + for (ClassInfo data : uncontrolled.keySet()) { + generator.generate(data); + } + + Set generatedTypes = new HashSet<>(); + generatedTypes.addAll(generator.getGeneratedTypes()); + + ExtensionMethodGenerator extensionMethodGenerator = new ExtensionMethodGenerator(classOutput); + for (TemplateExtensionMethodBuildItem templateExtension : templateExtensionMethods) { + extensionMethodGenerator.generate(templateExtension.getMethod(), templateExtension.getMatchName()); + } + generatedTypes.addAll(extensionMethodGenerator.getGeneratedTypes()); + + LOGGER.debugf("Generated types: %s", generatedTypes); + + for (String generateType : generatedTypes) { + generatedResolvers.produce(new GeneratedValueResolverBuildItem(generateType)); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, generateType)); + } + } + + @BuildStep + void collectTemplates(ApplicationArchivesBuildItem applicationArchivesBuildItem, + BuildProducer watchedPaths, + BuildProducer templatePaths, + BuildProducer nativeImageResources) + throws IOException { + ApplicationArchive applicationArchive = applicationArchivesBuildItem.getRootArchive(); + String basePath = "templates/"; + Path templatesPath = applicationArchive.getChildPath(basePath); + + if (templatesPath != null) { + scan(templatesPath, templatesPath, basePath, watchedPaths, templatePaths, nativeImageResources); + } + + String tagBasePath = basePath + "tags/"; + Path tagsPath = applicationArchive.getChildPath(tagBasePath); + if (tagsPath != null) { + try (Stream tagFiles = Files.list(tagsPath)) { + Iterator iter = tagFiles.filter(Files::isRegularFile) + .iterator(); + while (iter.hasNext()) { + Path path = iter.next(); + String tagPath = path.getFileName().toString(); + LOGGER.debugf("Found tag: %s", path); + produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, tagBasePath, tagPath, path, + true); + } + } + + } + } + + @BuildStep + void validateTemplateInjectionPoints(QuteConfig config, List templatePaths, + ValidationPhaseBuildItem validationPhase, + BuildProducer validationErrors) { + + Set filePaths = new HashSet(); + for (TemplatePathBuildItem templatePath : templatePaths) { + String path = templatePath.getPath(); + filePaths.add(path); + // Also add version without suffix from the path + // For example for "items.html" also add "items" + for (String suffix : config.suffixes) { + if (path.endsWith(suffix)) { + filePaths.add(path.substring(0, path.length() - (suffix.length() + 1))); + } + } + } + + for (InjectionPointInfo injectionPoint : validationPhase.getContext().get(BuildExtension.Key.INJECTION_POINTS)) { + + if (injectionPoint.getRequiredType().name().equals(TEMPLATE)) { + + AnnotationInstance resourcePath = injectionPoint.getRequiredQualifier(RESOURCE_PATH); + String name; + if (resourcePath != null) { + name = resourcePath.value().asString(); + } else if (injectionPoint.hasDefaultedQualifier()) { + name = getName(injectionPoint); + } else { + name = null; + } + if (name != null) { + // For "@Inject Template items" we try to match "items" + // For "@ResourcePath("github/pulls") Template pulls" we try to match "github/pulls" + if (filePaths.stream().noneMatch(path -> path.endsWith(name))) { + validationErrors.produce(new ValidationErrorBuildItem( + new IllegalStateException("No template found for " + injectionPoint.getTargetInfo()))); + } + } + + } else if (injectionPoint.getRequiredType().name().equals(VARIANT_TEMPLATE)) { + + AnnotationInstance resourcePath = injectionPoint.getRequiredQualifier(RESOURCE_PATH); + String name; + if (resourcePath != null) { + name = resourcePath.value().asString(); + } else if (injectionPoint.hasDefaultedQualifier()) { + name = getName(injectionPoint); + } else { + name = null; + } + if (name != null) { + if (filePaths.stream().noneMatch(path -> path.endsWith(name))) { + validationErrors.produce(new ValidationErrorBuildItem( + new IllegalStateException("No variant template found for " + injectionPoint.getTargetInfo()))); + } + } + } + } + } + + @BuildStep + TemplateVariantsBuildItem collectTemplateVariants(List templatePaths) throws IOException { + Set allPaths = templatePaths.stream().map(TemplatePathBuildItem::getPath).collect(Collectors.toSet()); + // item -> [item.html, item.txt] + Map> baseToVariants = new HashMap<>(); + for (String path : allPaths) { + int idx = path.lastIndexOf('.'); + if (idx != -1) { + String base = path.substring(0, idx); + List variants = baseToVariants.get(base); + if (variants == null) { + variants = new ArrayList<>(); + baseToVariants.put(base, variants); + } + variants.add(path); + } + } + LOGGER.debugf("Variant templates found: %s", baseToVariants); + return new TemplateVariantsBuildItem(baseToVariants); + } + + @BuildStep + ServiceProviderBuildItem registerPublisherFactory() { + return new ServiceProviderBuildItem(PublisherFactory.class.getName(), RxjavaPublisherFactory.class.getName()); + } + + @BuildStep + @Record(RUNTIME_INIT) + void initialize(QuteRecorder recorder, QuteConfig config, + List generatedValueResolvers, List templatePaths, + Optional templateVariants, + BeanContainerBuildItem beanContainer, + List startedServices) { + + List templates = new ArrayList<>(); + List tags = new ArrayList<>(); + for (TemplatePathBuildItem templatePath : templatePaths) { + if (templatePath.isTag()) { + tags.add(templatePath.getPath()); + } else { + templates.add(templatePath.getPath()); + } + } + + recorder.initEngine(config, beanContainer.getValue(), generatedValueResolvers.stream() + .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), + templates, + tags); + + Map> variants; + if (templateVariants.isPresent()) { + variants = templateVariants.get().getVariants(); + } else { + variants = Collections.emptyMap(); + } + recorder.initVariants(beanContainer.getValue(), variants); + } + + private Type resolveType(AnnotationTarget member, Match match, IndexView index) { + Type matchType; + if (member.kind() == Kind.FIELD) { + matchType = member.asField().type(); + } else if (member.kind() == Kind.METHOD) { + matchType = member.asMethod().returnType(); + } else { + throw new IllegalStateException("Unsupported member type: " + member); + } + if (matchType.kind() == org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE + || matchType.kind() == org.jboss.jandex.Type.Kind.TYPE_VARIABLE) { + Set closure = Types.getTypeClosure(match.clazz, Types.buildResolvedMap( + match.type.asParameterizedType().arguments(), match.clazz.typeParameters(), + new HashMap<>(), index), index); + DotName declaringClassName = member.kind() == Kind.METHOD ? member.asMethod().declaringClass().name() + : member.asField().declaringClass().name(); + Type declaringType = closure.stream() + .filter(t -> t.name().equals(declaringClassName)).findAny() + .orElse(null); + if (declaringType != null + && declaringType.kind() == org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE) { + matchType = Types.resolveTypeParam(matchType, + Types.buildResolvedMap(declaringType.asParameterizedType().arguments(), + index.getClassByName(declaringType.name()).typeParameters(), + Collections.emptyMap(), + index), + index); + } + } + return matchType; + } + + void processHints(String helperHint, Match match, IndexView index) { + if (LoopSectionHelper.Factory.HINT.equals(helperHint)) { + // Iterable, Stream => Item + // Map => Entry + processLoopHint(match, index); + } + } + + void processLoopHint(Match match, IndexView index) { + Set closure = Types.getTypeClosure(match.clazz, Types.buildResolvedMap( + match.type.asParameterizedType().arguments(), match.clazz.typeParameters(), new HashMap<>(), index), index); + Type matchType = null; + Type iterableType = closure.stream().filter(t -> t.name().equals(ITERABLE)).findFirst().orElse(null); + if (iterableType != null) { + // Iterable => Item + matchType = iterableType.asParameterizedType().arguments().get(0); + } else { + Type streamType = closure.stream().filter(t -> t.name().equals(STREAM)).findFirst().orElse(null); + if (streamType != null) { + // Stream => Long + matchType = streamType.asParameterizedType().arguments().get(0); + } else { + Type mapType = closure.stream().filter(t -> t.name().equals(MAP)).findFirst().orElse(null); + if (mapType != null) { + // Entry => Entry + Type[] args = new Type[2]; + args[0] = mapType.asParameterizedType().arguments().get(0); + args[1] = mapType.asParameterizedType().arguments().get(1); + matchType = ParameterizedType.create(MAP_ENTRY, args, null); + } + } + } + if (matchType != null) { + match.type = matchType; + match.clazz = index.getClassByName(match.type.name()); + } else { + // TODO better error reporting + throw new IllegalStateException("Unable to process the loop section hint for type: " + match.type); + } + } + + static class Match { + ClassInfo clazz; + Type type; + } + + private AnnotationTarget findTemplateExtensionMethod(String name, ClassInfo matchClass, + List templateExtensionMethods) { + for (TemplateExtensionMethodBuildItem templateExtensionMethod : templateExtensionMethods) { + if (templateExtensionMethod.matchesClass(matchClass) && templateExtensionMethod.matchesName(name)) { + return templateExtensionMethod.getMethod(); + } + } + return null; + } + + private AnnotationTarget findProperty(String property, ClassInfo clazz, IndexView index) { + int start = property.indexOf("("); + if (start != -1) { + // TODO validate virtual method parameters? + property = property.substring(0, start); + } + while (clazz != null) { + // Fields + for (FieldInfo field : clazz.fields()) { + if (Modifier.isPublic(field.flags()) && !Modifier.isStatic(field.flags()) + && !ValueResolverGenerator.isSynthetic(field.flags()) && field.name().equals(property)) { + return field; + } + } + // Methods + for (MethodInfo method : clazz.methods()) { + if (Modifier.isPublic(method.flags()) && !Modifier.isStatic(method.flags()) + && !ValueResolverGenerator.isSynthetic(method.flags()) && (method.name().equals(property) + || ValueResolverGenerator.getPropertyName(method.name()).equals(property))) { + return method; + } + } + DotName superName = clazz.superName(); + if (superName == null || DotNames.OBJECT.equals(superName)) { + clazz = null; + } else { + clazz = index.getClassByName(clazz.superName()); + } + } + return null; + } + + private void processsTemplateData(IndexView index, AnnotationInstance templateData, AnnotationTarget annotationTarget, + Set controlled, Map uncontrolled) { + AnnotationValue targetValue = templateData.value("target"); + if (targetValue == null || targetValue.asClass().name().equals(ValueResolverGenerator.TEMPLATE_DATA)) { + controlled.add(annotationTarget.asClass()); + } else { + ClassInfo uncontrolledClass = index.getClassByName(targetValue.asClass().name()); + if (uncontrolledClass != null) { + uncontrolled.compute(uncontrolledClass, (c, v) -> { + if (v == null) { + return templateData; + } + // Merge annotation values + AnnotationValue ignoreValue = templateData.value("ignore"); + if (ignoreValue == null || !ignoreValue.equals(v.value("ignore"))) { + ignoreValue = AnnotationValue.createArrayValue("ignore", new AnnotationValue[] {}); + } + AnnotationValue propertiesValue = templateData.value("properties"); + if (propertiesValue == null || propertiesValue.equals(v.value("properties"))) { + propertiesValue = AnnotationValue.createBooleanValue("properties", false); + } + return AnnotationInstance.create(templateData.name(), templateData.target(), + new AnnotationValue[] { ignoreValue, propertiesValue }); + }); + } else { + LOGGER.warnf("@TemplateData#target() not available: %s", annotationTarget.asClass().name()); + } + } + } + + private Set collectInjectExpressions(TemplatesAnalysisBuildItem analysis) { + Set injectExpressions = new HashSet<>(); + for (TemplateAnalysis template : analysis.getAnalysis()) { + injectExpressions.addAll(collectInjectExpressions(template)); + } + return injectExpressions; + } + + private Set collectInjectExpressions(TemplateAnalysis analysis) { + Set injectExpressions = new HashSet<>(); + for (Expression expression : analysis.expressions) { + if (expression.literal != null) { + continue; + } + if (EngineProducer.INJECT_NAMESPACE.equals(expression.namespace)) { + injectExpressions.add(expression); + } + } + return injectExpressions; + } + + public static String getName(InjectionPointInfo injectionPoint) { + if (injectionPoint.isField()) { + return injectionPoint.getTarget().asField().name(); + } else if (injectionPoint.isParam()) { + String name = injectionPoint.getTarget().asMethod().parameterName(injectionPoint.getPosition()); + return name == null ? injectionPoint.getTarget().asMethod().name() : name; + } + throw new IllegalArgumentException(); + } + + private static void produceTemplateBuildItems(BuildProducer templatePaths, + BuildProducer watchedPaths, + BuildProducer nativeImageResources, String basePath, String filePath, + Path originalPath, + boolean tag) { + if (filePath.isEmpty()) { + return; + } + String fullPath = basePath + filePath; + // NOTE: we cannot just drop the template because a template param can be added + watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(fullPath, true)); + nativeImageResources.produce(new NativeImageResourceBuildItem(fullPath)); + templatePaths.produce(new TemplatePathBuildItem(filePath, originalPath, tag)); + } + + private void scan(Path root, Path directory, String basePath, BuildProducer watchedPaths, + BuildProducer templatePaths, + BuildProducer nativeImageResources) + throws IOException { + try (Stream files = Files.list(directory)) { + Iterator iter = files.iterator(); + while (iter.hasNext()) { + Path filePath = iter.next(); + if (Files.isRegularFile(filePath)) { + LOGGER.debugf("Found template: %s", filePath); + String templatePath = root.relativize(filePath).toString(); + if (File.separatorChar != '/') { + templatePath = templatePath.replace(File.separatorChar, '/'); + } + produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, basePath, templatePath, + filePath, + false); + } else if (Files.isDirectory(filePath) && !filePath.getFileName().toString().equals("tags")) { + LOGGER.debugf("Scan directory: %s", filePath); + scan(root, filePath, basePath, watchedPaths, templatePaths, nativeImageResources); + } + } + } + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java new file mode 100644 index 0000000000000..b7fd0c65b9798 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java @@ -0,0 +1,46 @@ +package io.quarkus.qute.deployment; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.qute.TemplateExtension; + +/** + * Represents a template extension method. + * + * @see TemplateExtension + */ +public final class TemplateExtensionMethodBuildItem extends MultiBuildItem { + + private final MethodInfo method; + private final String matchName; + private final ClassInfo matchClass; + + public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, ClassInfo matchClass) { + this.method = method; + this.matchName = matchName; + this.matchClass = matchClass; + } + + public MethodInfo getMethod() { + return method; + } + + public String getMatchName() { + return matchName; + } + + public ClassInfo getMatchClass() { + return matchClass; + } + + public boolean matchesClass(ClassInfo clazz) { + return matchClass.name().equals(clazz.name()); + } + + public boolean matchesName(String name) { + return TemplateExtension.ANY.equals(matchName) ? true : matchName.equals(name); + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java new file mode 100644 index 0000000000000..76745c01c3da8 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatePathBuildItem.java @@ -0,0 +1,52 @@ +package io.quarkus.qute.deployment; + +import java.nio.file.Path; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Represents a template path. + */ +public final class TemplatePathBuildItem extends MultiBuildItem { + + private final String path; + private final Path fullPath; + private final boolean tag; + + public TemplatePathBuildItem(String path, Path fullPath, boolean tag) { + this.path = path; + this.fullPath = fullPath; + this.tag = tag; + } + + /** + * Uses the {@code /} path separator. + * + * @return the path relative to the template root + */ + public String getPath() { + return path; + } + + /** + * Uses the system-dependent path separator. + * + * @return the full path of the template + */ + public Path getFullPath() { + return fullPath; + } + + /** + * + * @return {@code true} if it represents a user tag, {@code false} otherwise + */ + public boolean isTag() { + return tag; + } + + public boolean isRegular() { + return !isTag(); + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateVariantsBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateVariantsBuildItem.java new file mode 100644 index 0000000000000..ffd2c4c5b4b66 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateVariantsBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.qute.deployment; + +import java.util.List; +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Holds all template variants found. + */ +public final class TemplateVariantsBuildItem extends SimpleBuildItem { + + private final Map> variants; + + public TemplateVariantsBuildItem(Map> variants) { + this.variants = variants; + } + + public Map> getVariants() { + return variants; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java new file mode 100644 index 0000000000000..a2123ebd08fcd --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java @@ -0,0 +1,38 @@ +package io.quarkus.qute.deployment; + +import java.util.List; +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.qute.Expression; + +/** + * Represents the result of analysis of all templates. + */ +public final class TemplatesAnalysisBuildItem extends SimpleBuildItem { + + private final List analysis; + + public TemplatesAnalysisBuildItem(List analysis) { + this.analysis = analysis; + } + + public List getAnalysis() { + return analysis; + } + + static class TemplateAnalysis { + + public final String id; + public final Set expressions; + public final TemplatePathBuildItem path; + + public TemplateAnalysis(String id, Set expressions, TemplatePathBuildItem path) { + this.id = id; + this.expressions = expressions; + this.path = path; + } + + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeCheckExcludeBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeCheckExcludeBuildItem.java new file mode 100644 index 0000000000000..ca5fc436beb0b --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeCheckExcludeBuildItem.java @@ -0,0 +1,24 @@ +package io.quarkus.qute.deployment; + +import java.util.function.BiPredicate; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Makes it possible to intentionally ignore some classes when performing type-safe checking. + */ +public final class TypeCheckExcludeBuildItem extends MultiBuildItem { + + private final BiPredicate predicate; + + public TypeCheckExcludeBuildItem(BiPredicate predicate) { + this.predicate = predicate; + } + + public BiPredicate getPredicate() { + return predicate; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeCheckInfo.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeCheckInfo.java new file mode 100644 index 0000000000000..3dc876626618b --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeCheckInfo.java @@ -0,0 +1,129 @@ +package io.quarkus.qute.deployment; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.function.Function; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; + +import io.quarkus.qute.Expression; +import io.quarkus.qute.Expressions; +import io.quarkus.qute.TemplateException; + +class TypeCheckInfo { + + static TypeCheckInfo create(Expression expression, IndexView index, Function templateIdToPathFun) { + String value = expression.typeCheckInfo; + List parts; + Map helperHints; + Type resolvedType; + ClassInfo rawClass; + int partsIdx = value.indexOf(RIGHT_BRACKET); + if (partsIdx + 1 < value.length()) { + String partsStr = value + .substring(partsIdx + 1, value.length()); + parts = new ArrayList<>(Expressions.splitParts(partsStr)); + helperHints = new HashMap<>(); + // [java.util.List].name + String firstPart = parts.get(0); + if (firstPart.equals(helperHint(firstPart))) { + parts.remove(0); + helperHints.put(ROOT_HINT, firstPart); + } + for (ListIterator iterator = parts.listIterator(); iterator.hasNext();) { + String part = iterator.next(); + String hint = helperHint(part); + if (hint != null) { + String val = part.substring(0, part.indexOf(LEFT_ANGLE)); + helperHints.put(val, hint); + iterator.set(val); + } + } + } else { + parts = Collections.emptyList(); + helperHints = Collections.emptyMap(); + } + String classStr = value.substring(1, value.indexOf("]")); + if (classStr.equals(Expressions.TYPECHECK_NAMESPACE_PLACEHOLDER)) { + rawClass = null; + resolvedType = null; + } else { + // "java.util.List" from [java.util.List].name + DotName rawClassName = rawClassName(classStr); + rawClass = index.getClassByName(rawClassName); + if (rawClass == null) { + throw new TemplateException( + "Class [" + rawClassName + "] used in the parameter declaration in template [" + + templateIdToPathFun.apply(expression.origin.getTemplateId()) + "] on line " + + expression.origin.getLine() + + " was not found in the application index. Make sure it is spelled correctly."); + } + resolvedType = resolveType(classStr); + } + return new TypeCheckInfo(resolvedType, rawClass, parts, helperHints); + } + + static final String LEFT_ANGLE = "<"; + static final String RIGHT_ANGLE = ">"; + static final String RIGHT_BRACKET = "]"; + static final String ROOT_HINT = "$$root$$"; + + final Type resolvedType; + final ClassInfo rawClass; + final List parts; + final Map helperHints; + + TypeCheckInfo(Type resolvedType, ClassInfo rawClass, List parts, Map helperHints) { + this.resolvedType = resolvedType; + this.rawClass = rawClass; + this.parts = parts; + this.helperHints = helperHints; + } + + String getHelperHint(String part) { + return helperHints.get(part); + } + + static DotName rawClassName(String value) { + int angleIdx = value.indexOf(LEFT_ANGLE); + if (angleIdx != -1) { + return DotName.createSimple(value.substring(0, angleIdx)); + } else { + return DotName.createSimple(value); + } + } + + static String helperHint(String part) { + int angleIdx = part.indexOf(LEFT_ANGLE); + if (angleIdx == -1) { + return null; + } + return part.substring(angleIdx, part.length()); + } + + static Type resolveType(String value) { + int angleIdx = value.indexOf(LEFT_ANGLE); + if (angleIdx == -1) { + return Type.create(DotName.createSimple(value), Kind.CLASS); + } else { + String name = value.substring(0, angleIdx); + DotName rawName = DotName.createSimple(name); + String[] parts = value.substring(angleIdx + 1, value.length() - 1).split(","); + Type[] arguments = new Type[parts.length]; + for (int i = 0; i < arguments.length; i++) { + arguments[i] = resolveType(parts[i]); + } + return ParameterizedType.create(rawName, arguments, null); + } + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java new file mode 100644 index 0000000000000..046b307f27128 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java @@ -0,0 +1,92 @@ +package io.quarkus.qute.deployment; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; +import org.jboss.jandex.TypeVariable; + +public final class Types { + + static Set getTypeClosure(ClassInfo classInfo, Map resolvedTypeParameters, + IndexView index) { + Set types = new HashSet<>(); + List typeParameters = classInfo.typeParameters(); + + if (typeParameters.isEmpty() || !typeParameters.stream().allMatch(resolvedTypeParameters::containsKey)) { + // Not a parameterized type or a raw type + types.add(Type.create(classInfo.name(), Kind.CLASS)); + } else { + // Canonical ParameterizedType with unresolved type variables + Type[] typeParams = new Type[typeParameters.size()]; + for (int i = 0; i < typeParameters.size(); i++) { + typeParams[i] = resolvedTypeParameters.get(typeParameters.get(i)); + } + types.add(ParameterizedType.create(classInfo.name(), typeParams, null)); + } + // Interfaces + for (Type interfaceType : classInfo.interfaceTypes()) { + ClassInfo interfaceClassInfo = index.getClassByName(interfaceType.name()); + if (interfaceClassInfo != null) { + Map resolved = Collections.emptyMap(); + if (Kind.PARAMETERIZED_TYPE.equals(interfaceType.kind())) { + resolved = buildResolvedMap(interfaceType.asParameterizedType().arguments(), + interfaceClassInfo.typeParameters(), resolvedTypeParameters, index); + } + types.addAll(getTypeClosure(interfaceClassInfo, resolved, index)); + } + } + // Superclass + if (classInfo.superClassType() != null) { + ClassInfo superClassInfo = index.getClassByName(classInfo.superName()); + if (superClassInfo != null) { + Map resolved = Collections.emptyMap(); + if (Kind.PARAMETERIZED_TYPE.equals(classInfo.superClassType().kind())) { + resolved = buildResolvedMap(classInfo.superClassType().asParameterizedType().arguments(), + superClassInfo.typeParameters(), + resolvedTypeParameters, index); + } + types.addAll(getTypeClosure(superClassInfo, resolved, index)); + } + } + return types; + } + + static Map buildResolvedMap(List resolvedArguments, + List typeVariables, + Map resolvedTypeParameters, IndexView index) { + Map resolvedMap = new HashMap<>(); + for (int i = 0; i < resolvedArguments.size(); i++) { + resolvedMap.put(typeVariables.get(i), resolveTypeParam(resolvedArguments.get(i), resolvedTypeParameters, index)); + } + return resolvedMap; + } + + static Type resolveTypeParam(Type typeParam, Map resolvedTypeParameters, IndexView index) { + if (typeParam.kind() == Kind.TYPE_VARIABLE) { + return resolvedTypeParameters.getOrDefault(typeParam, typeParam); + } else if (typeParam.kind() == Kind.PARAMETERIZED_TYPE) { + ParameterizedType parameterizedType = typeParam.asParameterizedType(); + ClassInfo classInfo = index.getClassByName(parameterizedType.name()); + if (classInfo != null) { + List typeParameters = classInfo.typeParameters(); + List arguments = parameterizedType.arguments(); + Type[] typeParams = new Type[typeParameters.size()]; + for (int i = 0; i < typeParameters.size(); i++) { + typeParams[i] = resolveTypeParam(arguments.get(i), resolvedTypeParameters, index); + } + return ParameterizedType.create(parameterizedType.name(), typeParams, null); + } + } + return typeParam; + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/EscapingTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/EscapingTest.java new file mode 100644 index 0000000000000..40488586bde26 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/EscapingTest.java @@ -0,0 +1,66 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.RawString; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateData; +import io.quarkus.test.QuarkusUnitTest; + +public class EscapingTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Item.class) + .addAsResource(new StringAsset("{text} {other} {text.raw} {text.safe} {item.foo}"), + "templates/foo.html") + .addAsResource(new StringAsset("{item} {item.raw}"), + "templates/item.html") + .addAsResource(new StringAsset("{text} {other} {text.raw} {text.safe} {item.foo}"), + "templates/bar.txt")); + + @Inject + Template foo; + + @Inject + Template bar; + + @Inject + Template item; + + @Test + public void testEscaper() { + assertEquals("<div> &"'

", + foo.data("text", "
").data("other", "&\"'").data("item", new Item()).render()); + // No escaping for txt templates + assertEquals("
&\"'
", + bar.data("text", "
").data("other", "&\"'").data("item", new Item()).render()); + // Item.toString() is escaped too + assertEquals("<h1>Item</h1>

Item

", + item.data("item", new Item()).render()); + } + + @TemplateData + public static class Item { + + public RawString getFoo() { + return new RawString(""); + } + + @Override + public String toString() { + return "

Item

"; + } + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/Foo.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/Foo.java new file mode 100644 index 0000000000000..0b1df01eaeb18 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/Foo.java @@ -0,0 +1,30 @@ +package io.quarkus.qute.deployment; + +class Foo { + + public String name; + + public Long age; + + public Charlie charlie; + + public Foo(String name, Long age) { + this.name = name; + this.age = age; + this.charlie = new Charlie(name.toUpperCase()); + } + + public static class Charlie { + + private String name; + + public Charlie(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + } +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/Hello.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/Hello.java new file mode 100644 index 0000000000000..7ea645cf0da04 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/Hello.java @@ -0,0 +1,14 @@ +package io.quarkus.qute.deployment; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Named; + +@ApplicationScoped +@Named +public class Hello { + + public String ping() { + return "pong"; + } + +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/HelloReflect.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/HelloReflect.java new file mode 100644 index 0000000000000..9bd38b765709a --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/HelloReflect.java @@ -0,0 +1,11 @@ +package io.quarkus.qute.deployment; + +public class HelloReflect { + + public Long age = 10l; + + public String ping() { + return "pong"; + } + +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/InjectNamespaceResolverTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/InjectNamespaceResolverTest.java new file mode 100644 index 0000000000000..e91b2185e1f5a --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/InjectNamespaceResolverTest.java @@ -0,0 +1,41 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.enterprise.context.Dependent; +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.test.QuarkusUnitTest; + +public class InjectNamespaceResolverTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(SimpleBean.class, Hello.class) + .addAsResource(new StringAsset("{inject:hello.ping}"), "templates/foo.html")); + + @Inject + SimpleBean simpleBean; + + @Test + public void testInjection() { + assertEquals("pong", simpleBean.foo.render()); + } + + @Dependent + public static class SimpleBean { + + @Inject + Template foo; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/InjectionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/InjectionTest.java new file mode 100644 index 0000000000000..c9b0960737a0a --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/InjectionTest.java @@ -0,0 +1,59 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import javax.enterprise.context.Dependent; +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.Template; +import io.quarkus.qute.api.ResourcePath; +import io.quarkus.test.QuarkusUnitTest; + +public class InjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(SimpleBean.class) + .addAsResource(new StringAsset("quarkus.qute.suffixes=txt"), "application.properties") + .addAsResource(new StringAsset("{this}"), "templates/foo.txt") + .addAsResource(new StringAsset("{this}"), "templates/foo.qute.html") + .addAsResource(new StringAsset("{this}"), "templates/bars/bar.txt")); + + @Inject + SimpleBean simpleBean; + + @Test + public void testInjection() { + assertNotNull(simpleBean.engine); + assertEquals("bar", simpleBean.foo.render("bar")); + assertEquals("bar", simpleBean.foo2.render("bar")); + assertEquals("bar", simpleBean.bar.render("bar")); + } + + @Dependent + public static class SimpleBean { + + @Inject + Engine engine; + + @Inject + Template foo; + + @ResourcePath("foo.qute.html") + Template foo2; + + @ResourcePath("bars/bar") + Template bar; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanNotFoundTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanNotFoundTest.java new file mode 100644 index 0000000000000..fb1b765cc1b42 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanNotFoundTest.java @@ -0,0 +1,27 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class NamedBeanNotFoundTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset("{inject:bing.ping}"), "templates/bing.html")) + .setExpectedException(TemplateException.class); + + @Test + public void testValidation() { + fail(); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanPropertyNotFoundTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanPropertyNotFoundTest.java new file mode 100644 index 0000000000000..0ff680f76a8b6 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanPropertyNotFoundTest.java @@ -0,0 +1,43 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Named; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class NamedBeanPropertyNotFoundTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(NamedFoo.class) + .addAsResource(new StringAsset("{inject:foo.list.ping}"), "templates/fooping.html")) + .setExpectedException(TemplateException.class); + + @Test + public void testValidation() { + fail(); + } + + @ApplicationScoped + @Named("foo") + public static class NamedFoo { + + public List getList() { + return null; + } + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/ParamDeclarationWrongClassTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/ParamDeclarationWrongClassTest.java new file mode 100644 index 0000000000000..0ca8432f0aff1 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/ParamDeclarationWrongClassTest.java @@ -0,0 +1,29 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class ParamDeclarationWrongClassTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Foo.class) + .addAsResource(new StringAsset("{@org.acme.Foo foo}" + + "{foo.name}"), "templates/foo.html")) + .setExpectedException(TemplateException.class); + + @Test + public void testValidation() { + fail(); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundTest.java new file mode 100644 index 0000000000000..ecb2a3a74d60a --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundTest.java @@ -0,0 +1,37 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class PropertyNotFoundTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Foo.class) + .addAsResource(new StringAsset("{@io.quarkus.qute.deployment.PropertyNotFoundTest$Foo foo}" + + "{foo.surname}"), "templates/foo.html")) + .setExpectedException(TemplateException.class); + + @Test + public void testValidation() { + fail(); + } + + static class Foo { + + public String name; + + public Long age; + + } + +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/FallbackTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/ReflectionResolverTest.java similarity index 53% rename from extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/FallbackTest.java rename to extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/ReflectionResolverTest.java index 5ffd3debeff4a..0a6c17ed063b1 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/FallbackTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/ReflectionResolverTest.java @@ -1,30 +1,32 @@ -package io.quarkus.smallrye.faulttolerance.test.fallback; +package io.quarkus.qute.deployment; import static org.junit.jupiter.api.Assertions.assertEquals; import javax.inject.Inject; import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.smallrye.faulttolerance.test.fallback.FallbackBean.RecoverFallback; +import io.quarkus.qute.Template; import io.quarkus.test.QuarkusUnitTest; -public class FallbackTest { +public class ReflectionResolverTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClasses(FallbackBean.class)); + .addClasses(HelloReflect.class) + .addAsResource(new StringAsset("{age}:{ping}:{noMatch}"), "templates/reflect.txt")); @Inject - FallbackBean bean; + Template reflect; @Test - public void testFallback() { - assertEquals(RecoverFallback.class.getName(), bean.ping()); + public void testInjection() { + assertEquals("10:pong:NOT_FOUND", reflect.render(new HelloReflect())); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataTest.java new file mode 100644 index 0000000000000..c5e8e569d9045 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateDataTest.java @@ -0,0 +1,50 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateData; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateDataTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Foo.class) + .addAsResource(new StringAsset("{foo.val} is not {foo.val.setScale(2,roundingMode)}"), + "templates/foo.txt")); + + @Inject + Template foo; + + @Test + public void testTemplateData() { + assertEquals("123.4563 is not 123.46", + foo.data("roundingMode", RoundingMode.HALF_UP).data("foo", new Foo(new BigDecimal("123.4563"))).render()); + } + + @TemplateData + @TemplateData(target = BigDecimal.class) + public static class Foo { + + public final BigDecimal val; + + public Foo(BigDecimal val) { + this.val = val; + } + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateExtensionMethodsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateExtensionMethodsTest.java new file mode 100644 index 0000000000000..55e99c70c52be --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateExtensionMethodsTest.java @@ -0,0 +1,90 @@ +package io.quarkus.qute.deployment; + +import static io.quarkus.qute.TemplateExtension.ANY; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateExtension; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateExtensionMethodsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Foo.class, Extensions.class) + .addAsResource(new StringAsset("{foo.name.toLower} {foo.name.ignored} {foo.callMe(1)} {foo.baz}"), + "templates/foo.txt") + .addAsResource(new StringAsset("{baz.setScale(2,roundingMode)}"), + "templates/baz.txt") + .addAsResource(new StringAsset("{anyInt.foo('bing')}"), + "templates/any.txt")); + + @Inject + Template foo; + + @Inject + Engine engine; + + @Test + public void testTemplateExtensions() { + assertEquals("fantomas NOT_FOUND 11 baz", + foo.data("foo", new Foo("Fantomas", 10l)).render()); + } + + @Test + public void testMethodParameters() { + assertEquals("123.46", + engine.getTemplate("baz.txt").data("roundingMode", RoundingMode.HALF_UP).data("baz", new BigDecimal("123.4563")) + .render()); + } + + @Test + public void testMatchAnyWithParameter() { + assertEquals("10=bing", + engine.getTemplate("any.txt").data("anyInt", 10).render()); + } + + @TemplateExtension + public static class Extensions { + + String ignored(String val) { + return val.toLowerCase(); + } + + static String toLower(String val) { + return val.toLowerCase(); + } + + static Long callMe(Foo foo, Integer val) { + return foo.age + val; + } + + @TemplateExtension(matchName = "baz") + static String override(Foo foo) { + return "baz"; + } + + static BigDecimal setScale(BigDecimal val, int scale, RoundingMode mode) { + return val.setScale(scale, mode); + } + + @TemplateExtension(matchName = ANY) + static String any(Integer val, String name, String info) { + return val + "=" + info; + } + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopFailureTest.java new file mode 100644 index 0000000000000..0fb45a18d5561 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopFailureTest.java @@ -0,0 +1,31 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class TypeSafeLoopFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Foo.class) + .addAsResource(new StringAsset("{@java.util.List list}" + + "{#for foo in list}" + + "{foo.name}={foo.ages}" + + "{/}"), "templates/foo.html")) + .setExpectedException(TemplateException.class); + + @Test + public void testValidation() { + fail(); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopTest.java new file mode 100644 index 0000000000000..f6c6473608ccb --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopTest.java @@ -0,0 +1,38 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.test.QuarkusUnitTest; + +public class TypeSafeLoopTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Foo.class) + .addAsResource(new StringAsset("{@java.util.List list}" + + "{#for foo in list}" + + "{foo.name}={foo.age}={foo.charlie.name}" + + "{/}"), "templates/foo.html")); + + @Inject + Template foo; + + @Test + public void testValidation() { + assertEquals("bravo=10=BRAVO", + foo.data("list", Collections.singletonList(new Foo("bravo", 10l))).render()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/VariantTemplateTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/VariantTemplateTest.java new file mode 100644 index 0000000000000..a6eb5d35e4f0b --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/VariantTemplateTest.java @@ -0,0 +1,48 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.enterprise.context.Dependent; +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Variant; +import io.quarkus.qute.api.VariantTemplate; +import io.quarkus.test.QuarkusUnitTest; + +public class VariantTemplateTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(SimpleBean.class) + .addAsResource(new StringAsset("{this}"), "templates/foo.txt") + .addAsResource(new StringAsset("{this}"), "templates/foo.html")); + + @Inject + SimpleBean simpleBean; + + @Test + public void testRendering() { + TemplateInstance rendering = simpleBean.foo.instance().data("bar"); + rendering.setAttribute(VariantTemplate.SELECTED_VARIANT, new Variant(null, "text/plain", null)); + assertEquals("bar", rendering.render()); + rendering.setAttribute(VariantTemplate.SELECTED_VARIANT, new Variant(null, "text/html", null)); + assertEquals("bar", rendering.render()); + } + + @Dependent + public static class SimpleBean { + + @Inject + VariantTemplate foo; + + } + +} diff --git a/extensions/qute/pom.xml b/extensions/qute/pom.xml new file mode 100644 index 0000000000000..27f9ba75a6858 --- /dev/null +++ b/extensions/qute/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + + quarkus-qute-parent + Quarkus - Qute + pom + + + deployment + runtime + + + diff --git a/extensions/qute/runtime/pom.xml b/extensions/qute/runtime/pom.xml new file mode 100644 index 0000000000000..26e14b62c45a9 --- /dev/null +++ b/extensions/qute/runtime/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + quarkus-qute-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-qute + Quarkus - Qute - Runtime + Qute is a templating engine designed specifically to meet the Quarkus needs + + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + io.quarkus.qute + qute-core + + + io.quarkus.qute + qute-rxjava + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/ResourcePath.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/ResourcePath.java new file mode 100644 index 0000000000000..c06865a57dc26 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/ResourcePath.java @@ -0,0 +1,50 @@ +package io.quarkus.qute.api; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.enterprise.util.Nonbinding; +import javax.inject.Qualifier; + +/** + * Qualifies an injected template. The {@link #value()} is used to locate the template; it represents the path relative from + * the base path. + */ +@Qualifier +@Retention(RUNTIME) +@Target({ FIELD, PARAMETER, METHOD }) +public @interface ResourcePath { + + /** + * @return the path relative from the base path, must not be an empty string + */ + @Nonbinding + String value(); + + /** + * Supports inline instantiation of this qualifier. + */ + public static final class Literal extends AnnotationLiteral implements ResourcePath { + + private static final long serialVersionUID = 1L; + + private final String value; + + public Literal(String value) { + this.value = value; + } + + @Override + public String value() { + return value; + } + + } + +} \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/VariantTemplate.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/VariantTemplate.java new file mode 100644 index 0000000000000..77b4f3e74f1c3 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/api/VariantTemplate.java @@ -0,0 +1,22 @@ +package io.quarkus.qute.api; + +import io.quarkus.qute.Template; +import io.quarkus.qute.Variant; + +/** + * + * @see Variant + */ +public interface VariantTemplate extends Template { + + /** + * Attribute key - all template {@link Variant}s found. + */ + String VARIANTS = "variants"; + + /** + * Attribute key - a selected {@link Variant}. + */ + String SELECTED_VARIANT = "selectedVariant"; + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/DefaultTemplateExtensions.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/DefaultTemplateExtensions.java new file mode 100644 index 0000000000000..068b6962ad1ef --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/DefaultTemplateExtensions.java @@ -0,0 +1,39 @@ +package io.quarkus.qute.runtime; + +import static io.quarkus.qute.TemplateExtension.ANY; + +import java.util.Map; + +import io.quarkus.qute.Results.Result; +import io.quarkus.qute.TemplateExtension; + +public class DefaultTemplateExtensions { + + @SuppressWarnings("rawtypes") + @TemplateExtension(matchName = ANY) + public static Object map(Map map, String name) { + Object val = map.get(name); + if (val != null) { + return val; + } + switch (name) { + case "keys": + case "keySet": + return map.keySet(); + case "values": + return map.values(); + case "size": + return map.size(); + case "empty": + case "isEmpty": + return map.isEmpty(); + case "get": + return map.get(name); + case "containsKey": + return map.containsKey(name); + default: + return Result.NOT_FOUND; + } + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java new file mode 100644 index 0000000000000..9cafeb77e2912 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -0,0 +1,241 @@ +package io.quarkus.qute.runtime; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Optional; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.qute.Engine; +import io.quarkus.qute.EngineBuilder; +import io.quarkus.qute.Escaper; +import io.quarkus.qute.Expression; +import io.quarkus.qute.NamespaceResolver; +import io.quarkus.qute.RawString; +import io.quarkus.qute.ReflectionValueResolver; +import io.quarkus.qute.ResultMapper; +import io.quarkus.qute.Results.Result; +import io.quarkus.qute.TemplateLocator.TemplateLocation; +import io.quarkus.qute.TemplateNode.Origin; +import io.quarkus.qute.UserTagSectionHelper; +import io.quarkus.qute.ValueResolver; +import io.quarkus.qute.ValueResolvers; +import io.quarkus.qute.Variant; + +@Singleton +public class EngineProducer { + + public static final String INJECT_NAMESPACE = "inject"; + + private static final Logger LOGGER = Logger.getLogger(EngineProducer.class); + + @Inject + Event event; + + private Engine engine; + private List tags; + + private List suffixes; + private String basePath; + private String tagPath; + + void init(QuteConfig config, List resolverClasses, List templatePaths, List tags) { + if (engine != null) { + LOGGER.warn("Qute already initialized!"); + return; + } + LOGGER.debugf("Initializing Qute with: %s", resolverClasses); + + suffixes = config.suffixes; + basePath = "templates/"; + tagPath = basePath + "tags/"; + + EngineBuilder builder = Engine.builder() + .addDefaultSectionHelpers(); + + // We don't register the map resolver because of param declaration validation + // See DefaultTemplateExtensions + builder.addValueResolver(ValueResolvers.thisResolver()); + builder.addValueResolver(ValueResolvers.orResolver()); + builder.addValueResolver(ValueResolvers.trueResolver()); + builder.addValueResolver(ValueResolvers.collectionResolver()); + builder.addValueResolver(ValueResolvers.mapperResolver()); + builder.addValueResolver(ValueResolvers.mapEntryResolver()); + // foo.string.raw returns a RawString which is never escaped + builder.addValueResolver(ValueResolvers.rawResolver()); + + // Escape some characters for HTML templates + Escaper htmlEscaper = Escaper.builder().add('"', """).add('\'', "'") + .add('&', "&").add('<', "<").add('>', ">").build(); + builder.addResultMapper(new ResultMapper() { + + @Override + public boolean appliesTo(Origin origin, Object result) { + return !(result instanceof RawString) + && origin.getVariant().filter(EngineProducer::requiresDefaultEscaping).isPresent(); + } + + @Override + public String map(Object result, Expression expression) { + return htmlEscaper.escape(result.toString()); + } + }); + + // Fallback reflection resolver + builder.addValueResolver(new ReflectionValueResolver()); + + // Allow anyone to customize the builder + event.fire(builder); + + // Resolve @Named beans + builder.addNamespaceResolver(NamespaceResolver.builder(INJECT_NAMESPACE).resolve(ctx -> { + InstanceHandle bean = Arc.container().instance(ctx.getName()); + return bean.isAvailable() ? bean.get() : Result.NOT_FOUND; + }).build()); + + // Add generated resolvers + for (String resolverClass : resolverClasses) { + builder.addValueResolver(createResolver(resolverClass)); + LOGGER.debugf("Added generated value resolver: %s", resolverClass); + } + // Add tags + this.tags = tags; + for (String tag : tags) { + // Strip suffix, item.html -> item + String tagName = tag.contains(".") ? tag.substring(0, tag.lastIndexOf('.')) : tag; + LOGGER.debugf("Registered UserTagSectionHelper for %s", tagName); + builder.addSectionHelper(new UserTagSectionHelper.Factory(tagName)); + } + // Add locator + builder.addLocator(this::locate); + engine = builder.build(); + + // Load discovered templates + for (String path : templatePaths) { + engine.getTemplate(path); + } + } + + @Produces + @ApplicationScoped + Engine getEngine() { + return engine; + } + + String getBasePath() { + return basePath; + } + + String getTagPath() { + return tagPath; + } + + List getSuffixes() { + return suffixes; + } + + private ValueResolver createResolver(String resolverClassName) { + try { + Class resolverClazz = Thread.currentThread() + .getContextClassLoader().loadClass(resolverClassName); + if (ValueResolver.class.isAssignableFrom(resolverClazz)) { + return (ValueResolver) resolverClazz.newInstance(); + } + throw new IllegalStateException("Not a value resolver: " + resolverClassName); + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { + throw new IllegalStateException("Unable to create resolver: " + resolverClassName, e); + } + } + + /** + * @param path + * @return the optional reader + */ + private Optional locate(String path) { + URL resource = null; + // First try to locate a tag template + if (tags.stream().anyMatch(tag -> tag.startsWith(path))) { + LOGGER.debugf("Locate tag for %s", path); + resource = locatePath(tagPath + path); + // Try path with suffixes + for (String suffix : suffixes) { + resource = locatePath(tagPath + path + "." + suffix); + if (resource != null) { + break; + } + } + } + if (resource == null) { + String templatePath = basePath + path; + LOGGER.debugf("Locate template for %s", templatePath); + resource = locatePath(templatePath); + } + if (resource != null) { + return Optional.of(new ResourceTemplateLocation(resource, guessVariant(path))); + } + return Optional.empty(); + } + + private URL locatePath(String path) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = EngineProducer.class.getClassLoader(); + } + return cl.getResource(path); + } + + static Variant guessVariant(String path) { + // TODO we need a proper way to detect the variant + int suffixIdx = path.lastIndexOf('.'); + if (suffixIdx != -1) { + String suffix = path.substring(suffixIdx); + return new Variant(null, VariantTemplateProducer.parseMediaType(suffix), null); + } + return null; + } + + static boolean requiresDefaultEscaping(Variant variant) { + return variant.mediaType != null + ? (Variant.TEXT_HTML.equals(variant.mediaType) || Variant.TEXT_XML.equals(variant.mediaType)) + : false; + } + + static class ResourceTemplateLocation implements TemplateLocation { + + private final URL resource; + private final Optional variant; + + public ResourceTemplateLocation(URL resource, Variant variant) { + this.resource = resource; + this.variant = Optional.ofNullable(variant); + } + + @Override + public Reader read() { + try { + return new InputStreamReader(resource.openStream(), Charset.forName("utf-8")); + } catch (IOException e) { + return null; + } + } + + @Override + public Optional getVariant() { + return variant; + } + + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java new file mode 100644 index 0000000000000..9fb4dfd5ddfa8 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java @@ -0,0 +1,22 @@ +package io.quarkus.qute.runtime; + +import java.util.List; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class QuteConfig { + + /** + * The set of suffixes used when attempting to locate a template file. + * + * By default, `engine.getTemplate("foo")` would result in several lookups: `foo`, `foo.html`, `foo.txt`, etc. + * + * @asciidoclet + */ + @ConfigItem(defaultValue = "qute.html,qute.txt,html,txt") + public List suffixes; + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java new file mode 100644 index 0000000000000..311d77e06064a --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java @@ -0,0 +1,23 @@ +package io.quarkus.qute.runtime; + +import java.util.List; +import java.util.Map; + +import io.quarkus.arc.runtime.BeanContainer; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class QuteRecorder { + + public void initEngine(QuteConfig config, BeanContainer container, List resolverClasses, + List templatePaths, List tags) { + EngineProducer producer = container.instance(EngineProducer.class); + producer.init(config, resolverClasses, templatePaths, tags); + } + + public void initVariants(BeanContainer container, Map> variants) { + VariantTemplateProducer producer = container.instance(VariantTemplateProducer.class); + producer.init(variants); + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java new file mode 100644 index 0000000000000..b599e7bd5d907 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -0,0 +1,116 @@ +package io.quarkus.qute.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.AnnotatedParameter; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.logging.Logger; + +import io.quarkus.qute.Expression; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Variant; +import io.quarkus.qute.api.ResourcePath; + +@Singleton +public class TemplateProducer { + + private static final Logger LOGGER = Logger.getLogger(TemplateProducer.class); + + @Inject + EngineProducer engineProducer; + + @Produces + Template getDefaultTemplate(InjectionPoint injectionPoint) { + String name = null; + if (injectionPoint.getMember() instanceof Field) { + // For "@Inject Template items" use "items" + name = injectionPoint.getMember().getName(); + } else { + AnnotatedParameter parameter = (AnnotatedParameter) injectionPoint.getAnnotated(); + if (parameter.getJavaParameter().isNamePresent()) { + name = parameter.getJavaParameter().getName(); + } else { + name = injectionPoint.getMember().getName(); + LOGGER.warnf("Parameter name not present - using the method name as the template name instead %s", name); + } + } + // Note that engine may not be initialized and so we inject a delegating template + return new InjectableTemplate(name, engineProducer.getSuffixes()); + } + + @Produces + @ResourcePath("ignored") + Template getTemplate(InjectionPoint injectionPoint) { + ResourcePath path = null; + for (Annotation qualifier : injectionPoint.getQualifiers()) { + if (qualifier.annotationType().equals(ResourcePath.class)) { + path = (ResourcePath) qualifier; + break; + } + } + if (path == null || path.value().isEmpty()) { + throw new IllegalStateException("No template reource path specified"); + } + // Note that engine may not be initialized and so we inject a delegating template + return new InjectableTemplate(path.value(), engineProducer.getSuffixes()); + } + + class InjectableTemplate implements Template { + + private final Supplier